From 648cd7c1afff4cd81f14539935c4578f07b95db0 Mon Sep 17 00:00:00 2001 From: aymericdelab Date: Tue, 11 Feb 2025 15:55:42 +0100 Subject: [PATCH 01/10] [client] one hex = one army Fixes #2677 --- .../game/src/three/managers/army-manager.ts | 46 ++- .../game/src/three/managers/biome-colors.ts | 21 + client/apps/game/src/three/managers/biome.ts | 194 --------- .../apps/game/src/three/managers/minimap.ts | 20 +- .../apps/game/src/three/scenes/constants.ts | 2 +- .../game/src/three/scenes/hexagon-scene.ts | 3 +- .../apps/game/src/three/scenes/hexception.tsx | 8 +- .../apps/game/src/three/scenes/worldmap.tsx | 53 ++- .../game/src/three/systems/system-manager.ts | 4 + client/apps/game/src/three/types/systems.ts | 3 +- .../src/managers/army-movement-manager.ts | 169 +++++--- packages/core/src/utils/biome/biome.ts | 90 +++++ packages/core/src/utils/biome/fixed-point.ts | 382 ++++++++++++++++++ packages/core/src/utils/biome/index.ts | 1 + .../core/src/utils/biome/simplex-noise.ts | 126 ++++++ packages/core/src/utils/biome/vec3.ts | 67 +++ packages/core/src/utils/biome/vec4.ts | 70 ++++ packages/core/src/utils/index.ts | 2 + packages/core/src/utils/travel-path.ts | 51 +++ 19 files changed, 1008 insertions(+), 304 deletions(-) create mode 100644 client/apps/game/src/three/managers/biome-colors.ts delete mode 100644 client/apps/game/src/three/managers/biome.ts create mode 100644 packages/core/src/utils/biome/biome.ts create mode 100644 packages/core/src/utils/biome/fixed-point.ts create mode 100644 packages/core/src/utils/biome/index.ts create mode 100644 packages/core/src/utils/biome/simplex-noise.ts create mode 100644 packages/core/src/utils/biome/vec3.ts create mode 100644 packages/core/src/utils/biome/vec4.ts create mode 100644 packages/core/src/utils/travel-path.ts diff --git a/client/apps/game/src/three/managers/army-manager.ts b/client/apps/game/src/three/managers/army-manager.ts index 4531f4e47..28a8b44c8 100644 --- a/client/apps/game/src/three/managers/army-manager.ts +++ b/client/apps/game/src/three/managers/army-manager.ts @@ -1,12 +1,18 @@ import { useAccountStore } from "@/hooks/store/use-account-store"; import { GUIManager } from "@/three/helpers/gui-manager"; -import { findShortestPath } from "@/three/helpers/pathfinding"; import { isAddressEqualToAccount } from "@/three/helpers/utils"; import { ArmyModel } from "@/three/managers/army-model"; -import { Biome } from "@/three/managers/biome"; import { LabelManager } from "@/three/managers/label-manager"; import { Position } from "@/types/position"; -import { BiomeType, ContractAddress, FELT_CENTER, ID, orders } from "@bibliothecadao/eternum"; +import { + ArmyMovementManager, + Biome, + BiomeType, + ContractAddress, + FELT_CENTER, + ID, + orders, +} from "@bibliothecadao/eternum"; import * as THREE from "three"; import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer"; import { ArmyData, ArmySystemUpdate, MovingArmyData, MovingLabelData, RenderChunkSize } from "../types"; @@ -28,22 +34,20 @@ export class ArmyManager { private currentChunkKey: string | null = "190,170"; private renderChunkSize: RenderChunkSize; private visibleArmies: ArmyData[] = []; - private biome: Biome; private armyPaths: Map = new Map(); - private exploredTiles: Map>; + private exploredTiles: Map>; private entityIdLabels: Map = new Map(); constructor( scene: THREE.Scene, renderChunkSize: { width: number; height: number }, - exploredTiles: Map>, + exploredTiles: Map>, ) { this.scene = scene; this.armyModel = new ArmyModel(scene); this.scale = new THREE.Vector3(0.3, 0.3, 0.3); this.labelManager = new LabelManager("textures/army_label.png", 1.5); this.renderChunkSize = renderChunkSize; - this.biome = new Biome(); this.exploredTiles = exploredTiles; this.onMouseMove = this.onMouseMove.bind(this); this.onRightClick = this.onRightClick.bind(this); @@ -124,7 +128,7 @@ export class ArmyManager { } } - async onUpdate(update: ArmySystemUpdate) { + async onUpdate(update: ArmySystemUpdate, armyHexes: Map>) { await this.armyModel.loadPromise; const { entityId, hexCoords, owner, battleId, currentHealth, order } = update; @@ -149,7 +153,7 @@ export class ArmyManager { const position = new Position({ x: hexCoords.col, y: hexCoords.row }); if (this.armies.has(entityId)) { - this.moveArmy(entityId, position); + this.moveArmy(entityId, position, armyHexes); } else { this.addArmy(entityId, position, owner, order); } @@ -182,7 +186,7 @@ export class ArmyManager { // this.armyModel.dummyObject.updateMatrix(); // Determine model type based on order or other criteria const { x, y } = army.hexCoords.getContract(); - const biome = this.biome.getBiome(x, y); + const biome = Biome.getBiome(x, y); if (biome === BiomeType.Ocean || biome === BiomeType.DeepOcean) { this.armyModel.assignModelToEntity(army.entityId, "boat"); } else { @@ -283,7 +287,7 @@ export class ArmyManager { // Determine model type based on order or other criteria const { x, y } = hexCoords.getContract(); - const biome = this.biome.getBiome(x, y); + const biome = Biome.getBiome(x, y); if (biome === BiomeType.Ocean || biome === BiomeType.DeepOcean) { this.armyModel.assignModelToEntity(entityId, "boat"); } else { @@ -303,7 +307,7 @@ export class ArmyManager { this.renderVisibleArmies(this.currentChunkKey!); } - public moveArmy(entityId: ID, hexCoords: Position) { + public moveArmy(entityId: ID, hexCoords: Position, armyHexes: Map>) { const armyData = this.armies.get(entityId); if (!armyData) return; @@ -312,14 +316,21 @@ export class ArmyManager { if (startX === targetX && startY === targetY) return; - const path = findShortestPath(armyData.hexCoords, hexCoords, this.exploredTiles); + const path = ArmyMovementManager.findPath( + { col: armyData.hexCoords.getContract().x, row: armyData.hexCoords.getContract().y }, + { col: hexCoords.getContract().x, row: hexCoords.getContract().y }, + armyHexes, + this.exploredTiles, + ); + + // const path = findShortestPath(armyData.hexCoords, hexCoords, this.exploredTiles); if (!path || path.length === 0) return; // Set initial direction before movement starts const firstHex = path[0]; const currentPosition = this.getArmyWorldPosition(entityId, armyData.hexCoords); - const newPosition = this.getArmyWorldPosition(entityId, firstHex); + const newPosition = this.getArmyWorldPosition(entityId, new Position({ x: firstHex.col, y: firstHex.row })); const direction = new THREE.Vector3().subVectors(newPosition, currentPosition).normalize(); const angle = Math.atan2(direction.x, direction.z); @@ -327,7 +338,10 @@ export class ArmyManager { // Update army position immediately to avoid starting from a "back" position this.armies.set(entityId, { ...armyData, hexCoords }); - this.armyPaths.set(entityId, path); + this.armyPaths.set( + entityId, + path.map((hex) => new Position({ x: hex.col, y: hex.row })), + ); const modelData = this.armyModel.getModelForEntity(entityId); if (modelData) { @@ -414,7 +428,7 @@ export class ArmyManager { } const { col, row } = getHexForWorldPosition({ x: position.x, y: position.y, z: position.z }); - const biome = this.biome.getBiome(col + FELT_CENTER, row + FELT_CENTER); + const biome = Biome.getBiome(col + FELT_CENTER, row + FELT_CENTER); if (biome === BiomeType.Ocean || biome === BiomeType.DeepOcean) { this.armyModel.assignModelToEntity(entityId, "boat"); } else { diff --git a/client/apps/game/src/three/managers/biome-colors.ts b/client/apps/game/src/three/managers/biome-colors.ts new file mode 100644 index 000000000..9bfb1f22e --- /dev/null +++ b/client/apps/game/src/three/managers/biome-colors.ts @@ -0,0 +1,21 @@ +import { BiomeType } from "@bibliothecadao/eternum"; +import * as THREE from "three"; + +export const BIOME_COLORS: Record = { + DeepOcean: new THREE.Color("#4a6b63"), + Ocean: new THREE.Color("#657d71"), + Beach: new THREE.Color("#d7b485"), + Scorched: new THREE.Color("#393131"), + Bare: new THREE.Color("#d1ae7f"), + Tundra: new THREE.Color("#cfd4d4"), + Snow: new THREE.Color("#cfd4d4"), + TemperateDesert: new THREE.Color("#ad6c44"), + Shrubland: new THREE.Color("#c1aa7f"), + Taiga: new THREE.Color("#292d23"), + Grassland: new THREE.Color("#6f7338"), + TemperateDeciduousForest: new THREE.Color("#6f7338"), + TemperateRainForest: new THREE.Color("#6f573e"), + SubtropicalDesert: new THREE.Color("#926338"), + TropicalSeasonalForest: new THREE.Color("#897049"), + TropicalRainForest: new THREE.Color("#8a714a"), +}; diff --git a/client/apps/game/src/three/managers/biome.ts b/client/apps/game/src/three/managers/biome.ts deleted file mode 100644 index ac488cf46..000000000 --- a/client/apps/game/src/three/managers/biome.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { Fixed, FixedTrait } from "@/utils/biome/fixed-point"; -import { noise as snoise } from "@/utils/biome/simplex-noise"; -import { Vec3 } from "@/utils/biome/vec3"; -import * as THREE from "three"; - -const MAP_AMPLITUDE = FixedTrait.fromInt(60n); -const MOISTURE_OCTAVE = FixedTrait.fromInt(2n); -const ELEVATION_OCTAVES = [ - FixedTrait.fromInt(1n), // 1 - FixedTrait.fromRatio(1n, 4n), // 0.25 - FixedTrait.fromRatio(1n, 10n), // 0.1 -]; -const ELEVATION_OCTAVES_SUM = ELEVATION_OCTAVES.reduce((a, b) => a.add(b), FixedTrait.ZERO); - -export enum BiomeType { - DeepOcean = "DeepOcean", - Ocean = "Ocean", - Beach = "Beach", - Scorched = "Scorched", - Bare = "Bare", - Tundra = "Tundra", - Snow = "Snow", - TemperateDesert = "TemperateDesert", - Shrubland = "Shrubland", - Taiga = "Taiga", - Grassland = "Grassland", - TemperateDeciduousForest = "TemperateDeciduousForest", - TemperateRainForest = "TemperateRainForest", - SubtropicalDesert = "SubtropicalDesert", - TropicalSeasonalForest = "TropicalSeasonalForest", - TropicalRainForest = "TropicalRainForest", -} - -export const BIOME_COLORS: Record = { - DeepOcean: new THREE.Color("#4a6b63"), - Ocean: new THREE.Color("#657d71"), - Beach: new THREE.Color("#d7b485"), - Scorched: new THREE.Color("#393131"), - Bare: new THREE.Color("#d1ae7f"), - Tundra: new THREE.Color("#cfd4d4"), - Snow: new THREE.Color("#cfd4d4"), - TemperateDesert: new THREE.Color("#ad6c44"), - Shrubland: new THREE.Color("#c1aa7f"), - Taiga: new THREE.Color("#292d23"), - Grassland: new THREE.Color("#6f7338"), - TemperateDeciduousForest: new THREE.Color("#6f7338"), - TemperateRainForest: new THREE.Color("#6f573e"), - SubtropicalDesert: new THREE.Color("#926338"), - TropicalSeasonalForest: new THREE.Color("#897049"), - TropicalRainForest: new THREE.Color("#8a714a"), -}; - -const LEVEL = { - DEEP_OCEAN: FixedTrait.fromRatio(25n, 100n), // 0.25 - OCEAN: FixedTrait.fromRatio(50n, 100n), // 0.5 - SAND: FixedTrait.fromRatio(53n, 100n), // 0.53 - FOREST: FixedTrait.fromRatio(60n, 100n), // 0.6 - DESERT: FixedTrait.fromRatio(72n, 100n), // 0.72 - MOUNTAIN: FixedTrait.fromRatio(80n, 100n), // 0.8 -}; - -export class Biome { - constructor() {} - - getBiome(col: number, row: number): BiomeType { - const elevation = this.calculateElevation(col, row, MAP_AMPLITUDE, ELEVATION_OCTAVES, ELEVATION_OCTAVES_SUM); - const moisture = this.calculateMoisture(col, row, MAP_AMPLITUDE, MOISTURE_OCTAVE); - return this.determineBiome(elevation, moisture, LEVEL); - } - - private calculateElevation( - col: number, - row: number, - mapAmplitude: Fixed, - octaves: Fixed[], - octavesSum: Fixed, - ): Fixed { - let elevation = FixedTrait.ZERO; - let _100 = FixedTrait.fromInt(100n); - let _2 = FixedTrait.fromInt(2n); - for (const octave of octaves) { - let x = FixedTrait.fromInt(BigInt(col)).div(octave).div(mapAmplitude); - let z = FixedTrait.fromInt(BigInt(row)).div(octave).div(mapAmplitude); - - let sn = snoise(Vec3.new(x, FixedTrait.ZERO, z)); - const noise = sn.add(FixedTrait.ONE).mul(_100).div(_2); - elevation = elevation.add(octave.mul(noise.floor())); - } - - return elevation.div(octavesSum).div(FixedTrait.fromInt(100n)); - } - - private calculateMoisture(col: number, row: number, mapAmplitude: Fixed, moistureOctave: Fixed): Fixed { - const moistureX = moistureOctave.mul(FixedTrait.fromInt(BigInt(col))).div(mapAmplitude); - const moistureZ = moistureOctave.mul(FixedTrait.fromInt(BigInt(row))).div(mapAmplitude); - const noise = snoise(Vec3.new(moistureX, FixedTrait.ZERO, moistureZ)) - .add(FixedTrait.ONE) - .mul(FixedTrait.fromInt(100n)) - .div(FixedTrait.fromInt(2n)); - return FixedTrait.floor(noise).div(FixedTrait.fromInt(100n)); - } - - private determineBiome(elevation: Fixed, moisture: Fixed, level: typeof LEVEL): BiomeType { - if (elevation.value < level.DEEP_OCEAN.value) return BiomeType.DeepOcean; - if (elevation.value < level.OCEAN.value) return BiomeType.Ocean; - if (elevation.value < level.SAND.value) return BiomeType.Beach; - - if (elevation.value > level.MOUNTAIN.value) { - if (moisture.value < FixedTrait.fromRatio(10n, 100n).value) return BiomeType.Scorched; - if (moisture.value < FixedTrait.fromRatio(40n, 100n).value) return BiomeType.Bare; - if (moisture.value < FixedTrait.fromRatio(50n, 100n).value) return BiomeType.Tundra; - return BiomeType.Snow; - } - if (elevation.value > level.DESERT.value) { - if (moisture.value < FixedTrait.fromRatio(33n, 100n).value) return BiomeType.TemperateDesert; - if (moisture.value < FixedTrait.fromRatio(66n, 100n).value) return BiomeType.Shrubland; - return BiomeType.Taiga; - } - if (elevation.value > level.FOREST.value) { - if (moisture.value < FixedTrait.fromRatio(16n, 100n).value) return BiomeType.TemperateDesert; - if (moisture.value < FixedTrait.fromRatio(50n, 100n).value) return BiomeType.Grassland; - if (moisture.value < FixedTrait.fromRatio(83n, 100n).value) return BiomeType.TemperateDeciduousForest; - return BiomeType.TemperateRainForest; - } - if (moisture.value < FixedTrait.fromRatio(16n, 100n).value) return BiomeType.SubtropicalDesert; - if (moisture.value < FixedTrait.fromRatio(33n, 100n).value) return BiomeType.Grassland; - if (moisture.value < FixedTrait.fromRatio(66n, 100n).value) return BiomeType.TropicalSeasonalForest; - return BiomeType.TropicalRainForest; - } -} - -function analyzeBiomeDistribution(centerX: number, centerY: number, radius: number) { - const biome = new Biome(); - const biomeCounts: Record = { - DeepOcean: 0, - Ocean: 0, - Beach: 0, - Scorched: 0, - Bare: 0, - Tundra: 0, - Snow: 0, - TemperateDesert: 0, - Shrubland: 0, - Taiga: 0, - Grassland: 0, - TemperateDeciduousForest: 0, - TemperateRainForest: 0, - SubtropicalDesert: 0, - TropicalSeasonalForest: 0, - TropicalRainForest: 0, - }; - - const startX = centerX - radius; - const endX = centerX + radius; - const startY = centerY - radius; - const endY = centerY + radius; - let totalTiles = 0; - - for (let x = startX; x <= endX; x++) { - for (let y = startY; y <= endY; y++) { - const biomeType = biome.getBiome(x, y); - biomeCounts[biomeType]++; - totalTiles++; - } - } - - console.log(`\nBiome Distribution Analysis`); - console.log(`Center: (${centerX}, ${centerY})`); - console.log(`Radius: ${radius} tiles`); - console.log(`Total area: ${totalTiles} tiles\n`); - console.log("Biome Counts:"); - - Object.entries(biomeCounts) - .sort(([, a], [, b]) => b - a) - .forEach(([biomeType, count]) => { - const percentage = ((count / totalTiles) * 100).toFixed(2); - console.log(`${biomeType}: ${count} tiles (${percentage}%)`); - }); -} - -// Example usage: -// analyzeBiomeDistribution(2147483646, 2147483646, 500); - -// function testBiomeGeneration() { -// const biome = new Biome(); -// const start = 5000871265127650; -// const end = 5000871265127678; -// for (let i = start; i <= end; i++) { -// const result = biome.getBiome(i - 5, i + 10); -// console.log(`biome for ${i - 5} ${i + 10} is ${result} \n`); -// } -// } - -// testBiomeGeneration(); diff --git a/client/apps/game/src/three/managers/minimap.ts b/client/apps/game/src/three/managers/minimap.ts index 6c1965f7c..97f959435 100644 --- a/client/apps/game/src/three/managers/minimap.ts +++ b/client/apps/game/src/three/managers/minimap.ts @@ -1,11 +1,10 @@ import { useUIStore } from "@/hooks/store/use-ui-store"; import { type ArmyManager } from "@/three/managers/army-manager"; import { type BattleManager } from "@/three/managers/battle-manager"; -import { type Biome, BIOME_COLORS } from "@/three/managers/biome"; +import { BIOME_COLORS } from "@/three/managers/biome-colors"; import { type StructureManager } from "@/three/managers/structure-manager"; import type WorldmapScene from "@/three/scenes/worldmap"; -import { FELT_CENTER } from "@/ui/config"; -import { StructureType } from "@bibliothecadao/eternum"; +import { BiomeType, StructureType } from "@bibliothecadao/eternum"; import throttle from "lodash/throttle"; import type * as THREE from "three"; import { getHexForWorldPosition } from "../utils"; @@ -62,11 +61,10 @@ class Minimap { private canvas!: HTMLCanvasElement; private context!: CanvasRenderingContext2D; private camera!: THREE.PerspectiveCamera; - private exploredTiles!: Map>; + private exploredTiles!: Map>; private structureManager!: StructureManager; private armyManager!: ArmyManager; private battleManager!: BattleManager; - private biome!: Biome; private mapCenter: { col: number; row: number } = { col: 250, row: 150 }; private mapSize: { width: number; height: number } = { width: MINIMAP_CONFIG.MAP_COLS_WIDTH, @@ -93,18 +91,17 @@ class Minimap { constructor( worldmapScene: WorldmapScene, - exploredTiles: Map>, + exploredTiles: Map>, camera: THREE.PerspectiveCamera, structureManager: StructureManager, armyManager: ArmyManager, battleManager: BattleManager, - biome: Biome, ) { this.worldmapScene = worldmapScene; this.waitForMinimapElement().then((canvas) => { this.canvas = canvas; this.loadLabelImages(); - this.initializeCanvas(structureManager, exploredTiles, armyManager, biome, camera, battleManager); + this.initializeCanvas(structureManager, exploredTiles, armyManager, camera, battleManager); this.canvas.addEventListener("canvasResized", this.handleResize); }); } @@ -125,9 +122,8 @@ class Minimap { private initializeCanvas( structureManager: StructureManager, - exploredTiles: Map>, + exploredTiles: Map>, armyManager: ArmyManager, - biome: Biome, camera: THREE.PerspectiveCamera, battleManager: BattleManager, ) { @@ -136,7 +132,6 @@ class Minimap { this.exploredTiles = exploredTiles; this.armyManager = armyManager; this.battleManager = battleManager; - this.biome = biome; this.camera = camera; this.scaleX = this.canvas.width / this.mapSize.width; this.scaleY = this.canvas.height / this.mapSize.height; @@ -218,14 +213,13 @@ class Minimap { private drawExploredTiles() { this.exploredTiles.forEach((rows, col) => { - rows.forEach((row) => { + rows.forEach((biome, 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); } diff --git a/client/apps/game/src/three/scenes/constants.ts b/client/apps/game/src/three/scenes/constants.ts index 86d6fccad..567d9d471 100644 --- a/client/apps/game/src/three/scenes/constants.ts +++ b/client/apps/game/src/three/scenes/constants.ts @@ -1,6 +1,6 @@ -import { BiomeType } from "@/three/managers/biome"; import { IS_FLAT_MODE } from "@/ui/config"; import { + BiomeType, BuildingType, RealmLevelNames, RealmLevels, diff --git a/client/apps/game/src/three/scenes/hexagon-scene.ts b/client/apps/game/src/three/scenes/hexagon-scene.ts index 94dd40665..81d0d2489 100644 --- a/client/apps/game/src/three/scenes/hexagon-scene.ts +++ b/client/apps/game/src/three/scenes/hexagon-scene.ts @@ -2,7 +2,6 @@ import { useUIStore, type AppStore } from "@/hooks/store/use-ui-store"; import { GUIManager } from "@/three/helpers/gui-manager"; import { LocationManager } from "@/three/helpers/location-manager"; import { gltfLoader } from "@/three/helpers/utils"; -import { type BiomeType } from "@/three/managers/biome"; import { HighlightHexManager } from "@/three/managers/highlight-hex-manager"; import { InputManager } from "@/three/managers/input-manager"; import InstancedBiome from "@/three/managers/instanced-biome"; @@ -12,7 +11,7 @@ import { HEX_SIZE, biomeModelPaths } from "@/three/scenes/constants"; import { SystemManager } from "@/three/systems/system-manager"; import { LeftView, RightView } from "@/types"; import { GRAPHICS_SETTING, GraphicsSettings, IS_FLAT_MODE } from "@/ui/config"; -import { type HexPosition, type SetupResult } from "@bibliothecadao/eternum"; +import { BiomeType, type HexPosition, type SetupResult } from "@bibliothecadao/eternum"; import gsap from "gsap"; import throttle from "lodash/throttle"; import * as THREE from "three"; diff --git a/client/apps/game/src/three/scenes/hexception.tsx b/client/apps/game/src/three/scenes/hexception.tsx index 29e67b77f..ac7e41bcc 100644 --- a/client/apps/game/src/three/scenes/hexception.tsx +++ b/client/apps/game/src/three/scenes/hexception.tsx @@ -2,7 +2,7 @@ import { useAccountStore } from "@/hooks/store/use-account-store"; import { useUIStore } from "@/hooks/store/use-ui-store"; import { createHexagonShape } from "@/three/geometry/hexagon-geometry"; import { createPausedLabel, gltfLoader } from "@/three/helpers/utils"; -import { BIOME_COLORS, Biome, BiomeType } from "@/three/managers/biome"; +import { BIOME_COLORS } from "@/three/managers/biome-colors"; import { BuildingPreview } from "@/three/managers/building-preview"; import { SMALL_DETAILS_NAME } from "@/three/managers/instanced-model"; import { SceneManager } from "@/three/scene-manager"; @@ -14,6 +14,8 @@ import { IS_FLAT_MODE } from "@/ui/config"; import { ResourceIcon } from "@/ui/elements/resource-icon"; import { BUILDINGS_CENTER, + Biome, + BiomeType, BuildingType, HexPosition, RealmLevels, @@ -102,7 +104,6 @@ export default class HexceptionScene extends HexagonScene { private pillars: THREE.InstancedMesh | null = null; private buildings: any = []; centerColRow: number[] = [0, 0]; - private biome!: Biome; private highlights: { col: number; row: number }[] = []; private buildingPreview: BuildingPreview | null = null; private tileManager: TileManager; @@ -125,7 +126,6 @@ export default class HexceptionScene extends HexagonScene { ) { super(SceneName.Hexception, controls, dojo, mouse, raycaster, sceneManager); - this.biome = new Biome(); this.buildingPreview = new BuildingPreview(this.scene); const pillarGeometry = new THREE.ExtrudeGeometry(createHexagonShape(1), { depth: 2, bevelEnabled: false }); @@ -616,7 +616,7 @@ export default class HexceptionScene extends HexagonScene { existingBuildings: any[], biomeHexes: Record, ) => { - const biome = this.biome.getBiome(targetHex.col, targetHex.row); + const biome = Biome.getBiome(targetHex.col, targetHex.row); const buildableAreaBiome = "Grassland"; const isFlat = biome === "Ocean" || biome === "DeepOcean" || isMainHex; diff --git a/client/apps/game/src/three/scenes/worldmap.tsx b/client/apps/game/src/three/scenes/worldmap.tsx index 5ded856c1..4025d0bc4 100644 --- a/client/apps/game/src/three/scenes/worldmap.tsx +++ b/client/apps/game/src/three/scenes/worldmap.tsx @@ -4,7 +4,6 @@ import { useUIStore } from "@/hooks/store/use-ui-store"; import { LoadingStateKey } from "@/hooks/store/use-world-loading"; import { ArmyManager } from "@/three/managers/army-manager"; import { BattleManager } from "@/three/managers/battle-manager"; -import { Biome } from "@/three/managers/biome"; import Minimap from "@/three/managers/minimap"; import { SelectedHexManager } from "@/three/managers/selected-hex-manager"; import { StructureManager } from "@/three/managers/structure-manager"; @@ -20,6 +19,7 @@ import { UNDEFINED_STRUCTURE_ENTITY_ID } from "@/ui/constants"; import { getBlockTimestamp } from "@/utils/timestamp"; import { ArmyMovementManager, + Biome, BiomeType, DUMMY_HYPERSTRUCTURE_ENTITY_ID, HexPosition, @@ -53,7 +53,9 @@ export default class WorldmapScene extends HexagonScene { private armyManager: ArmyManager; private structureManager: StructureManager; private battleManager: BattleManager; - private exploredTiles: Map> = new Map(); + private exploredTiles: Map> = new Map(); + private armyHexes: Map> = new Map(); + private armiesPositions: Map = new Map(); private battles: Map> = new Map(); private tileManager: TileManager; private structurePreview: StructurePreview | null = null; @@ -82,8 +84,6 @@ export default class WorldmapScene extends HexagonScene { this.GUIFolder.add(this, "moveCameraToURLLocation"); - this.biome = new Biome(); - this.structurePreview = new StructurePreview(this.scene); this.tileManager = new TileManager(this.dojo.components, this.dojo.systemCalls, { col: 0, row: 0 }); @@ -119,7 +119,7 @@ export default class WorldmapScene extends HexagonScene { this.armySubscription?.unsubscribe(); this.armySubscription = this.systemManager.Army.onUpdate((update: ArmySystemUpdate) => { - this.armyManager.onUpdate(update).then((needsUpdate) => { + this.armyManager.onUpdate(update, this.armyHexes).then((needsUpdate) => { if (needsUpdate) { this.updateVisibleChunks(); } @@ -128,6 +128,7 @@ export default class WorldmapScene extends HexagonScene { this.systemManager.Battle.onUpdate((value) => this.battleManager.onUpdate(value)); this.systemManager.Tile.onUpdate((value) => this.updateExploredHex(value)); + this.systemManager.Army.onUpdate((value) => this.updateArmyHexes(value)); this.systemManager.Structure.onUpdate((value) => { const optimisticStructure = this.structureManager.structures.removeStructure( Number(DUMMY_HYPERSTRUCTURE_ENTITY_ID), @@ -180,7 +181,6 @@ export default class WorldmapScene extends HexagonScene { this.structureManager, this.armyManager, this.battleManager, - this.biome, ); } @@ -371,7 +371,15 @@ export default class WorldmapScene extends HexagonScene { ); const { currentDefaultTick, currentArmiesTick } = getBlockTimestamp(); - const travelPaths = armyMovementManager.findPaths(this.exploredTiles, currentDefaultTick, currentArmiesTick); + const armyStamina = 10; + + const travelPaths = armyMovementManager.findPaths( + this.armyHexes, + this.exploredTiles, + armyStamina, + currentDefaultTick, + currentArmiesTick, + ); this.state.updateTravelPaths(travelPaths.getPaths()); this.highlightHexManager.highlightHexes(travelPaths.getHighlightedHexes()); } @@ -408,8 +416,28 @@ export default class WorldmapScene extends HexagonScene { this.armyManager.removeLabelsFromScene(); } + public updateArmyHexes(update: ArmySystemUpdate) { + const { + hexCoords: { col, row }, + } = update; + const newCol = col - FELT_CENTER; + const newRow = row - FELT_CENTER; + const oldHexCoords = this.armiesPositions.get(update.entityId); + const oldCol = oldHexCoords?.col; + const oldRow = oldHexCoords?.row; + + if (oldCol !== newCol || oldRow !== newRow) { + if (oldCol && oldRow) { + this.armyHexes.get(oldCol)?.delete(oldRow); + } + if (newCol && newRow) { + this.armyHexes.get(newCol)?.add(newRow); + } + } + } + public async updateExploredHex(update: TileSystemUpdate) { - const { hexCoords, removeExplored } = update; + const { hexCoords, removeExplored, biome } = update; const col = hexCoords.col - FELT_CENTER; const row = hexCoords.row - FELT_CENTER; @@ -425,10 +453,10 @@ export default class WorldmapScene extends HexagonScene { } if (!this.exploredTiles.has(col)) { - this.exploredTiles.set(col, new Set()); + this.exploredTiles.set(col, new Map()); } if (!this.exploredTiles.get(col)!.has(row)) { - this.exploredTiles.get(col)!.add(row); + this.exploredTiles.get(col)!.set(row, biome); } const dummy = new THREE.Object3D(); @@ -454,9 +482,6 @@ export default class WorldmapScene extends HexagonScene { dummy.rotation.y = 0; } - const biomePosition = new Position({ x: col, y: row }).getContract(); - const biome = this.biome.getBiome(biomePosition.x, biomePosition.y); - dummy.updateMatrix(); const { chunkX, chunkZ } = this.worldToChunkCoordinates(pos.x, pos.z); @@ -672,7 +697,7 @@ export default class WorldmapScene extends HexagonScene { dummy.rotation.y = 0; } - const biome = this.biome.getBiome(startCol + col + FELT_CENTER, startRow + row + FELT_CENTER); + const biome = Biome.getBiome(startCol + col + FELT_CENTER, startRow + row + FELT_CENTER); dummy.updateMatrix(); diff --git a/client/apps/game/src/three/systems/system-manager.ts b/client/apps/game/src/three/systems/system-manager.ts index 9c5729e2f..6f610b4d8 100644 --- a/client/apps/game/src/three/systems/system-manager.ts +++ b/client/apps/game/src/three/systems/system-manager.ts @@ -1,5 +1,6 @@ import { Position } from "@/types/position"; import { + BiomeType, ClientComponents, configManager, divideByPrecision, @@ -280,10 +281,13 @@ export class SystemManager { const newState = update.value[0]; const prevState = update.value[1]; + console.log("newState", newState); + const { col, row } = prevState || newState; return { hexCoords: { col, row }, removeExplored: !newState, + biome: newState?.biome === "None" ? BiomeType.Grassland : newState?.biome || BiomeType.Grassland, }; }); }, diff --git a/client/apps/game/src/three/types/systems.ts b/client/apps/game/src/three/types/systems.ts index 6891ee301..a84a52b7c 100644 --- a/client/apps/game/src/three/types/systems.ts +++ b/client/apps/game/src/three/types/systems.ts @@ -1,5 +1,5 @@ import { Position } from "@/types/position"; -import { HexPosition, ID, StructureType } from "@bibliothecadao/eternum"; +import { BiomeType, HexPosition, ID, StructureType } from "@bibliothecadao/eternum"; import { StructureProgress } from "./common"; export type ArmySystemUpdate = { @@ -25,6 +25,7 @@ export type StructureSystemUpdate = { export type TileSystemUpdate = { hexCoords: HexPosition; removeExplored: boolean; + biome: BiomeType; }; export type BattleSystemUpdate = { diff --git a/packages/core/src/managers/army-movement-manager.ts b/packages/core/src/managers/army-movement-manager.ts index 36439b7c0..ca6855a12 100644 --- a/packages/core/src/managers/army-movement-manager.ts +++ b/packages/core/src/managers/army-movement-manager.ts @@ -4,6 +4,7 @@ import { uuid } from "@latticexyz/utils"; import { Account, AccountInterface } from "starknet"; import { DojoAccount } from ".."; import { + BiomeType, CapacityConfigCategory, FELT_CENTER, getDirectionBetweenAdjacentHexes, @@ -13,61 +14,13 @@ import { import { ClientComponents } from "../dojo/create-client-components"; import { EternumProvider } from "../provider"; import { ContractAddress, HexPosition, ID, TravelTypes } from "../types"; -import { multiplyByPrecision } from "../utils"; +import { Biome, multiplyByPrecision } from "../utils"; +import { TravelPaths } from "../utils/travel-path"; import { configManager } from "./config-manager"; import { ResourceManager } from "./resource-manager"; import { StaminaManager } from "./stamina-manager"; import { computeExploreFoodCosts, computeTravelFoodCosts, getRemainingCapacityInKg } from "./utils"; -export class TravelPaths { - private readonly paths: Map; - - constructor() { - this.paths = new Map(); - } - - set(key: string, value: { path: HexPosition[]; isExplored: boolean }): void { - this.paths.set(key, value); - } - - deleteAll(): void { - this.paths.clear(); - } - - get(key: string): { path: HexPosition[]; isExplored: boolean } | undefined { - return this.paths.get(key); - } - - has(key: string): boolean { - return this.paths.has(key); - } - - values(): IterableIterator<{ path: HexPosition[]; isExplored: boolean }> { - return this.paths.values(); - } - - getHighlightedHexes(): Array<{ col: number; row: number }> { - return Array.from(this.paths.values()).map(({ path }) => ({ - col: path[path.length - 1].col - FELT_CENTER, - row: path[path.length - 1].row - FELT_CENTER, - })); - } - - isHighlighted(row: number, col: number): boolean { - return this.paths.has(TravelPaths.posKey({ col: col + FELT_CENTER, row: row + FELT_CENTER })); - } - - getPaths(): Map { - return this.paths; - } - - static posKey(pos: HexPosition, normalized = false): string { - const col = normalized ? pos.col + FELT_CENTER : pos.col; - const row = normalized ? pos.row + FELT_CENTER : pos.row; - return `${col},${row}`; - } -} - export class ArmyMovementManager { private readonly entity: Entity; private readonly entityId: ID; @@ -146,8 +99,91 @@ export class ArmyMovementManager { }; } + public static staminaDrain(biome: BiomeType) { + if (biome === BiomeType.Grassland) { + return 1; + } + if (biome === BiomeType.Bare) { + return 2; + } + if (biome === BiomeType.Snow) { + return 3; + } + if (biome === BiomeType.Tundra) { + return 4; + } + return 0; + } + + public static findPath( + startPos: HexPosition, + endPos: HexPosition, + armyHexes: Map>, + exploredHexes: Map>, + ): HexPosition[] { + const priorityQueue: Array<{ position: HexPosition; staminaUsed: number; distance: number; path: HexPosition[] }> = + [{ position: startPos, staminaUsed: 0, distance: 0, path: [startPos] }]; + const shortestDistances = new Map(); + + while (priorityQueue.length > 0) { + priorityQueue.sort((a, b) => a.staminaUsed - b.staminaUsed || a.distance - b.distance); + const { position: current, staminaUsed, distance, path } = priorityQueue.shift()!; + const currentKey = TravelPaths.posKey(current); + + if (current.col === endPos.col && current.row === endPos.row) { + return path; + } + + const shortest = shortestDistances.get(currentKey); + if ( + !shortest || + staminaUsed < shortest.staminaUsed || + (staminaUsed === shortest.staminaUsed && distance < shortest.distance) + ) { + shortestDistances.set(currentKey, { distance, staminaUsed }); + const isExplored = exploredHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; + if (!isExplored) continue; + + const hasArmy = armyHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; + if (hasArmy) continue; + + const neighbors = getNeighborHexes(current.col, current.row); + for (const { col, row } of neighbors) { + const neighborKey = TravelPaths.posKey({ col, row }); + const nextDistance = distance + 1; + const nextPath = [...path, { col, row }]; + + const isExplored = exploredHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + const biome = exploredHexes.get(col - FELT_CENTER)?.get(row - FELT_CENTER); + const staminaCost = biome ? this.staminaDrain(biome) : 0; + const nextStaminaUsed = staminaUsed + staminaCost; + + if (isExplored) { + const shortest = shortestDistances.get(neighborKey); + if ( + !shortest || + nextStaminaUsed < shortest.staminaUsed || + (nextStaminaUsed === shortest.staminaUsed && nextDistance < shortest.distance) + ) { + priorityQueue.push({ + position: { col, row }, + staminaUsed: nextStaminaUsed, + distance: nextDistance, + path: nextPath, + }); + } + } + } + } + } + + return []; + } + public findPaths( - exploredHexes: Map>, + armyHexes: Map>, + exploredHexes: Map>, + armyStamina: number, currentDefaultTick: number, currentArmiesTick: number, ): TravelPaths { @@ -155,15 +191,14 @@ export class ArmyMovementManager { const maxHex = this._calculateMaxTravelPossible(currentDefaultTick, currentArmiesTick); const canExplore = this._canExplore(currentDefaultTick, currentArmiesTick); - const priorityQueue: Array<{ position: HexPosition; distance: number; path: HexPosition[] }> = [ - { position: startPos, distance: 0, path: [startPos] }, - ]; + const priorityQueue: Array<{ position: HexPosition; staminaUsed: number; distance: number; path: HexPosition[] }> = + [{ position: startPos, staminaUsed: 0, distance: 0, path: [startPos] }]; const travelPaths = new TravelPaths(); const shortestDistances = new Map(); while (priorityQueue.length > 0) { - priorityQueue.sort((a, b) => a.distance - b.distance); // This makes the queue work as a priority queue - const { position: current, distance, path } = priorityQueue.shift()!; + priorityQueue.sort((a, b) => a.distance - b.distance); + const { position: current, staminaUsed, distance, path } = priorityQueue.shift()!; const currentKey = TravelPaths.posKey(current); if (!shortestDistances.has(currentKey) || distance < shortestDistances.get(currentKey)!) { @@ -174,16 +209,32 @@ export class ArmyMovementManager { } if (!isExplored) continue; - const neighbors = getNeighborHexes(current.col, current.row); // This function needs to be defined + const hasArmy = armyHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; + console.log({ hasArmy }); + + if (hasArmy) continue; + + const neighbors = getNeighborHexes(current.col, current.row); for (const { col, row } of neighbors) { const neighborKey = TravelPaths.posKey({ col, row }); const nextDistance = distance + 1; const nextPath = [...path, { col, row }]; const isExplored = exploredHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + const biome = exploredHexes.get(col - FELT_CENTER)?.get(row - FELT_CENTER); + const staminaCost = biome ? ArmyMovementManager.staminaDrain(biome) : 0; + const nextStaminaUsed = staminaUsed + staminaCost; + + if (nextStaminaUsed > armyStamina) continue; + if ((isExplored && nextDistance <= maxHex) || (!isExplored && canExplore && nextDistance === 1)) { if (!shortestDistances.has(neighborKey) || nextDistance < shortestDistances.get(neighborKey)!) { - priorityQueue.push({ position: { col, row }, distance: nextDistance, path: nextPath }); + priorityQueue.push({ + position: { col, row }, + staminaUsed: nextStaminaUsed, + distance: nextDistance, + path: nextPath, + }); } } } @@ -238,7 +289,7 @@ export class ArmyMovementManager { row, explored_by_id: this.entityId, explored_at: BigInt(Math.floor(Date.now() / 1000)), - biome: "None", + biome: Biome.getBiome(col, row), }, }); }; diff --git a/packages/core/src/utils/biome/biome.ts b/packages/core/src/utils/biome/biome.ts new file mode 100644 index 000000000..8b2f87a46 --- /dev/null +++ b/packages/core/src/utils/biome/biome.ts @@ -0,0 +1,90 @@ +import { BiomeType } from "../../constants/hex"; +import { Fixed, FixedTrait } from "./fixed-point"; +import { noise as snoise } from "./simplex-noise"; +import { Vec3 } from "./vec3"; + +const MAP_AMPLITUDE = FixedTrait.fromInt(60n); +const MOISTURE_OCTAVE = FixedTrait.fromInt(2n); +const ELEVATION_OCTAVES = [ + FixedTrait.fromInt(1n), // 1 + FixedTrait.fromRatio(1n, 4n), // 0.25 + FixedTrait.fromRatio(1n, 10n), // 0.1 +]; +const ELEVATION_OCTAVES_SUM = ELEVATION_OCTAVES.reduce((a, b) => a.add(b), FixedTrait.ZERO); + +const LEVEL = { + DEEP_OCEAN: FixedTrait.fromRatio(25n, 100n), // 0.25 + OCEAN: FixedTrait.fromRatio(50n, 100n), // 0.5 + SAND: FixedTrait.fromRatio(53n, 100n), // 0.53 + FOREST: FixedTrait.fromRatio(60n, 100n), // 0.6 + DESERT: FixedTrait.fromRatio(72n, 100n), // 0.72 + MOUNTAIN: FixedTrait.fromRatio(80n, 100n), // 0.8 +}; + +export class Biome { + static getBiome(col: number, row: number): BiomeType { + const elevation = Biome.calculateElevation(col, row, MAP_AMPLITUDE, ELEVATION_OCTAVES, ELEVATION_OCTAVES_SUM); + const moisture = Biome.calculateMoisture(col, row, MAP_AMPLITUDE, MOISTURE_OCTAVE); + return Biome.determineBiome(elevation, moisture, LEVEL); + } + + private static calculateElevation( + col: number, + row: number, + mapAmplitude: Fixed, + octaves: Fixed[], + octavesSum: Fixed, + ): Fixed { + let elevation = FixedTrait.ZERO; + let _100 = FixedTrait.fromInt(100n); + let _2 = FixedTrait.fromInt(2n); + for (const octave of octaves) { + let x = FixedTrait.fromInt(BigInt(col)).div(octave).div(mapAmplitude); + let z = FixedTrait.fromInt(BigInt(row)).div(octave).div(mapAmplitude); + + let sn = snoise(Vec3.new(x, FixedTrait.ZERO, z)); + const noise = sn.add(FixedTrait.ONE).mul(_100).div(_2); + elevation = elevation.add(octave.mul(noise.floor())); + } + + return elevation.div(octavesSum).div(FixedTrait.fromInt(100n)); + } + + private static calculateMoisture(col: number, row: number, mapAmplitude: Fixed, moistureOctave: Fixed): Fixed { + const moistureX = moistureOctave.mul(FixedTrait.fromInt(BigInt(col))).div(mapAmplitude); + const moistureZ = moistureOctave.mul(FixedTrait.fromInt(BigInt(row))).div(mapAmplitude); + const noise = snoise(Vec3.new(moistureX, FixedTrait.ZERO, moistureZ)) + .add(FixedTrait.ONE) + .mul(FixedTrait.fromInt(100n)) + .div(FixedTrait.fromInt(2n)); + return FixedTrait.floor(noise).div(FixedTrait.fromInt(100n)); + } + + private static determineBiome(elevation: Fixed, moisture: Fixed, level: typeof LEVEL): BiomeType { + if (elevation.value < level.DEEP_OCEAN.value) return BiomeType.DeepOcean; + if (elevation.value < level.OCEAN.value) return BiomeType.Ocean; + if (elevation.value < level.SAND.value) return BiomeType.Beach; + + if (elevation.value > level.MOUNTAIN.value) { + if (moisture.value < FixedTrait.fromRatio(10n, 100n).value) return BiomeType.Scorched; + if (moisture.value < FixedTrait.fromRatio(40n, 100n).value) return BiomeType.Bare; + if (moisture.value < FixedTrait.fromRatio(50n, 100n).value) return BiomeType.Tundra; + return BiomeType.Snow; + } + if (elevation.value > level.DESERT.value) { + if (moisture.value < FixedTrait.fromRatio(33n, 100n).value) return BiomeType.TemperateDesert; + if (moisture.value < FixedTrait.fromRatio(66n, 100n).value) return BiomeType.Shrubland; + return BiomeType.Taiga; + } + if (elevation.value > level.FOREST.value) { + if (moisture.value < FixedTrait.fromRatio(16n, 100n).value) return BiomeType.TemperateDesert; + if (moisture.value < FixedTrait.fromRatio(50n, 100n).value) return BiomeType.Grassland; + if (moisture.value < FixedTrait.fromRatio(83n, 100n).value) return BiomeType.TemperateDeciduousForest; + return BiomeType.TemperateRainForest; + } + if (moisture.value < FixedTrait.fromRatio(16n, 100n).value) return BiomeType.SubtropicalDesert; + if (moisture.value < FixedTrait.fromRatio(33n, 100n).value) return BiomeType.Grassland; + if (moisture.value < FixedTrait.fromRatio(66n, 100n).value) return BiomeType.TropicalSeasonalForest; + return BiomeType.TropicalRainForest; + } +} diff --git a/packages/core/src/utils/biome/fixed-point.ts b/packages/core/src/utils/biome/fixed-point.ts new file mode 100644 index 000000000..11c75fc96 --- /dev/null +++ b/packages/core/src/utils/biome/fixed-point.ts @@ -0,0 +1,382 @@ +/** + * TypeScript port of ABDK Math 64.64 Library plus modifications + * Original: https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMath64x64.sol + * Original Copyright © 2019 by ABDK Consulting + * Original Author: Mikhail Vladimirov + * + * Modified by: Credence + * + * Modifications: + * - mul and div are handled differently from the original library, which may affect the precision. + */ + +// Constants +const MIN_64x64 = BigInt("0x80000000000000000000000000000000") * BigInt(-1); +const MAX_64x64 = BigInt("0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); +const ONE_64x64 = BigInt(1) << BigInt(64); + +export class Fixed { + public value: bigint; + + constructor(value: bigint) { + this.value = value; + } + + mul(other: Fixed): Fixed { + return FixedTrait.mul(this, other); + } + + div(other: Fixed): Fixed { + return FixedTrait.div(this, other); + } + + add(other: Fixed): Fixed { + return FixedTrait.add(this, other); + } + + sub(other: Fixed): Fixed { + return FixedTrait.sub(this, other); + } + + abs(): Fixed { + return FixedTrait.abs(this); + } + + sqrt(): Fixed { + return FixedTrait.sqrt(this); + } + + floor(): Fixed { + return FixedTrait.floor(this); + } + + mod(other: Fixed): Fixed { + return FixedTrait.mod(this, other); + } + + rem(other: Fixed): Fixed { + return FixedTrait.rem(this, other); + } + + neg(): Fixed { + return FixedTrait.neg(this); + } +} + +export class FixedTrait { + static ONE_64x64 = ONE_64x64; + + static ONE = new Fixed(ONE_64x64); + static ZERO = new Fixed(0n); + + /** + * Convert a ratio to a fixed point number + */ + static fromRatio(numerator: bigint, denominator: bigint): Fixed { + return FixedTrait.divi(FixedTrait.fromInt(numerator), FixedTrait.fromInt(denominator)); + } + + /** + * Convert signed integer to 64.64 fixed point + */ + static fromInt(x: bigint): Fixed { + if (x < -0x8000000000000000n || x > 0x7fffffffffffffffn) { + throw new Error("Input out of bounds"); + } + return new Fixed(x << 64n); + } + + /** + * Convert 64.64 fixed point to integer (rounding down) + */ + static toInt(x: Fixed): Fixed { + return new Fixed(x.value >> 64n); + } + + /** + * Convert unsigned integer to 64.64 fixed point + */ + static fromUInt(x: bigint): Fixed { + if (x > 0x7fffffffffffffffn) { + throw new Error("Input out of bounds"); + } + return new Fixed(x << 64n); + } + + /** + * Convert 64.64 fixed point to unsigned integer (rounding down) + */ + static toUInt(x: Fixed): bigint { + if (x.value < 0n) { + throw new Error("Input must be positive"); + } + return x.value >> 64n; + } + + /** + * Add two 64.64 fixed point numbers + */ + static add(x: Fixed, y: Fixed): Fixed { + const result = x.value + y.value; + if (result < MIN_64x64 || result > MAX_64x64) { + throw new Error("Overflow add"); + } + return new Fixed(result); + } + + /** + * Subtract two 64.64 fixed point numbers + */ + static sub(x: Fixed, y: Fixed): Fixed { + const result = x.value - y.value; + if (result < MIN_64x64 || result > MAX_64x64) { + throw new Error("Overflow sub"); + } + return new Fixed(result); + } + + /** + * Multiply two 64.64 fixed point numbers (rounding down) + * Handles sign separately from magnitude for better precision + * + * Note: This is a replica of the way it is handled in the cubit starknet library + * e.g of difference: + * + * ABDKMath64x64: -3952873730080618200n * 24233599860844537324 = 5192914255895257994 + * cubit: -3952873730080618200n * 24233599860844537324 = -5192914255895257993 + */ + static mul(x: Fixed, y: Fixed): Fixed { + // Extract signs + const xNegative = x.value < 0n; + const yNegative = y.value < 0n; + + // Work with absolute values + const xAbs = xNegative ? -x.value : x.value; + const yAbs = yNegative ? -y.value : y.value; + + // Perform multiplication and scaling + const result = (xAbs * yAbs) >> 64n; + + // Apply combined sign + const finalResult = xNegative !== yNegative ? -result : result; + + if (finalResult < MIN_64x64 || finalResult > MAX_64x64) { + throw new Error("Overflow mul"); + } + return new Fixed(finalResult); + } + + /** + * Divide two 64.64 fixed point numbers (rounding down) + * Handles sign separately from magnitude for better precision + * + * Note: This is a modified version of the original div function. + * It handles overflow differently, which may affect the precision. + * + * This is a replica of the way it is handled in the cubit starknet library + * e.g of difference: + */ + static div(x: Fixed, y: Fixed): Fixed { + if (y.value === 0n) { + throw new Error("Division by zero"); + } + + // Extract signs + const xNegative = x.value < 0n; + const yNegative = y.value < 0n; + + // Work with absolute values + const xAbs = xNegative ? -x.value : x.value; + const yAbs = yNegative ? -y.value : y.value; + + // Perform division with scaling + const result = (xAbs << 64n) / yAbs; + + // Apply combined sign + const finalResult = xNegative !== yNegative ? -result : result; + + if (finalResult < MIN_64x64 || finalResult > MAX_64x64) { + throw new Error("Overflow div"); + } + return new Fixed(finalResult); + } + + static divi(x: Fixed, y: Fixed): Fixed { + if (y.value === 0n) { + throw new Error("Division by zero"); + } + + let negativeResult = false; + if (x.value < 0n) { + x.value = -x.value; // We rely on overflow behavior here + negativeResult = true; + } + if (y.value < 0n) { + y.value = -y.value; // We rely on overflow behavior here + negativeResult = !negativeResult; + } + const absoluteResult = FixedTrait.divuu(x, y); + if (negativeResult) { + if (absoluteResult.value <= 0x80000000000000000000000000000000n) { + return new Fixed(-absoluteResult.value); // We rely on overflow behavior here + } else { + throw new Error("Overflow divi"); + } + } + return absoluteResult; + } + + static divuu(x: Fixed, y: Fixed): Fixed { + if (y.value === 0n) { + throw new Error("Division by zero"); + } + + let result; + + if (x.value <= 0xffffffffffffffffffffffffffffffffffffffffffffffffn) { + result = (x.value << 64n) / y.value; + } else { + let msb = 192n; + let xc = x.value >> 192n; + if (xc >= 0x100000000n) { + xc >>= 32n; + msb += 32n; + } + if (xc >= 0x10000n) { + xc >>= 16n; + msb += 16n; + } + if (xc >= 0x100n) { + xc >>= 8n; + msb += 8n; + } + if (xc >= 0x10n) { + xc >>= 4n; + msb += 4n; + } + if (xc >= 0x4n) { + xc >>= 2n; + msb += 2n; + } + if (xc >= 0x2n) msb += 1n; // No need to shift xc anymore + + result = (x.value << (255n - msb)) / (((y.value - 1n) >> (msb - 191n)) + 1n); + if (result > 0xffffffffffffffffffffffffffffffffn) { + throw new Error("Overflow divuu"); + } + + let hi = result * (y.value >> 128n); + let lo = result * (y.value & 0xffffffffffffffffffffffffffffffffn); + + let xh = x.value >> 192n; + let xl = x.value << 64n; + + if (xl < lo) xh -= 1n; + xl -= lo; // We rely on overflow behavior here + lo = hi << 128n; + if (xl < lo) xh -= 1n; + xl -= lo; // We rely on overflow behavior here + + result += xh == hi >> 128n ? xl / y.value : 1n; + } + + if (result > 0xffffffffffffffffffffffffffffffffn) { + throw new Error("Overflow divuu"); + } + return new Fixed(result); + } + + static divu(x: Fixed, y: Fixed): Fixed { + if (y.value === 0n) { + throw new Error("Division by zero"); + } + + const result = FixedTrait.divuu(x, y); + if (result.value > MAX_64x64) { + throw new Error("Overflow divu"); + } + return result; + } + + /** + * Calculate absolute value + */ + static abs(x: Fixed): Fixed { + if (x.value === MIN_64x64) { + throw new Error("Overflow abs"); + } + return x.value < 0n ? new Fixed(-x.value) : x; + } + + /** + * Calculate square root (rounding down) + */ + static sqrt(x: Fixed): Fixed { + if (x.value < 0n) { + throw new Error("Input must be positive"); + } + + if (x.value === 0n) return new Fixed(0n); + + // Initial estimate using integer square root + let r = 1n; + let xx = x.value; + + // Binary search for the square root + if (xx >= 1n << 128n) { + xx >>= 128n; + r <<= 64n; + } + if (xx >= 1n << 64n) { + xx >>= 64n; + r <<= 32n; + } + if (xx >= 1n << 32n) { + xx >>= 32n; + r <<= 16n; + } + if (xx >= 1n << 16n) { + xx >>= 16n; + r <<= 8n; + } + if (xx >= 1n << 8n) { + xx >>= 8n; + r <<= 4n; + } + if (xx >= 1n << 4n) { + xx >>= 4n; + r <<= 2n; + } + if (xx >= 1n << 2n) { + r <<= 1n; + } + + // Newton's method iterations + r = (r + x.value / r) >> 1n; + r = (r + x.value / r) >> 1n; + r = (r + x.value / r) >> 1n; + r = (r + x.value / r) >> 1n; + r = (r + x.value / r) >> 1n; + r = (r + x.value / r) >> 1n; + r = (r + x.value / r) >> 1n; + + const r1 = FixedTrait.div(x, new Fixed(r)); + return r < r1.value ? new Fixed(r) : r1; + } + + static floor(a: Fixed): Fixed { + return new Fixed((a.value >> 64n) * FixedTrait.ONE_64x64); + } + + static mod(a: Fixed, b: Fixed): Fixed { + return new Fixed(a.value % b.value); + } + + static rem(a: Fixed, b: Fixed): Fixed { + return new Fixed(a.value % b.value); + } + + static neg(a: Fixed): Fixed { + return new Fixed(-a.value); + } +} diff --git a/packages/core/src/utils/biome/index.ts b/packages/core/src/utils/biome/index.ts new file mode 100644 index 000000000..d2fd288bb --- /dev/null +++ b/packages/core/src/utils/biome/index.ts @@ -0,0 +1 @@ +export * from "./biome"; diff --git a/packages/core/src/utils/biome/simplex-noise.ts b/packages/core/src/utils/biome/simplex-noise.ts new file mode 100644 index 000000000..3500497ce --- /dev/null +++ b/packages/core/src/utils/biome/simplex-noise.ts @@ -0,0 +1,126 @@ +import { Fixed, FixedTrait } from "./fixed-point"; +import { Vec3 } from "./vec3"; +import { Vec4 } from "./vec4"; + +function permute(x: Vec4): Vec4 { + let v34 = Vec4.splat(new Fixed(627189298506124754944n)); + let v1 = Vec4.splat(FixedTrait.ONE); + let v289 = Vec4.splat(new Fixed(5331109037302060417024n)); + return x.mul(v34).add(v1).mul(x).mod(v289); +} + +function taylor_inv_sqrt(r: Vec4): Vec4 { + let v1: Vec4 = Vec4.splat(new Fixed(33072114398950993631n)); // 1.79284291400159 + let v2: Vec4 = Vec4.splat(new Fixed(15748625904262413056n)); // 0.85373472095314 + return v1.minus(v2.mul(r)); +} + +function step(edge: Fixed, x: Fixed): Fixed { + if (x.value < edge.value) { + return FixedTrait.ZERO; + } else { + return FixedTrait.ONE; + } +} + +function min(a: Fixed, b: Fixed): Fixed { + return a.value < b.value ? a : b; +} + +function max(a: Fixed, b: Fixed): Fixed { + return a.value > b.value ? a : b; +} + +export function noise(v: Vec3): Fixed { + let zero = new Fixed(0n); + let half = new Fixed(9223372036854775808n); // 0.5 + let one = FixedTrait.ONE; + + let Cx = new Fixed(3074457345618258602n); // 1 / 6 + let Cy = new Fixed(6148914691236517205n); // 1 / 3 + + // First corner + let i = v.add(Vec3.splat(v.dot(Vec3.splat(Cy)))).floor(); + let x0 = v.minus(i).add(Vec3.splat(i.dot(Vec3.splat(Cx)))); + + // Other corners + let g = Vec3.new(step(x0.y, x0.x), step(x0.z, x0.y), step(x0.x, x0.z)); + let l = Vec3.splat(one).minus(g); + let i1 = Vec3.new(min(g.x, l.z), min(g.y, l.x), min(g.z, l.y)); + let i2 = Vec3.new(max(g.x, l.z), max(g.y, l.x), max(g.z, l.y)); + + let x1 = Vec3.new(x0.x.sub(i1.x).add(Cx), x0.y.sub(i1.y).add(Cx), x0.z.sub(i1.z).add(Cx)); + let x2 = Vec3.new(x0.x.sub(i2.x).add(Cy), x0.y.sub(i2.y).add(Cy), x0.z.sub(i2.z).add(Cy)); + let x3 = Vec3.new(x0.x.sub(half), x0.y.sub(half), x0.z.sub(half)); + + // Permutations + i = i.remScaler(new Fixed(5331109037302060417024n)); // 289 + let _p1 = permute(Vec4.new(i.z.add(zero), i.z.add(i1.z), i.z.add(i2.z), i.z.add(one))); + let _p2 = permute( + Vec4.new(_p1.x.add(i.y).add(zero), _p1.y.add(i.y).add(i1.y), _p1.z.add(i.y).add(i2.y), _p1.w.add(i.y).add(one)), + ); + let p = permute( + Vec4.new(_p2.x.add(i.x).add(zero), _p2.y.add(i.x).add(i1.x), _p2.z.add(i.x).add(i2.x), _p2.w.add(i.x).add(one)), + ); + + // Gradients: 7x7 points over a square, mapped onto an octahedron. + // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294) + let ns_x = new Fixed(5270498306774157605n); // 2 / 7 + let ns_y = new Fixed(-17129119497016012214n); // -13 / 14 + let ns_z = new Fixed(2635249153387078803n); // 1 / 7 + + let j = p.remScaler(new Fixed(903890459611768029184n)); // 49 + let x_ = j.mulScalar(ns_z).floor(); + let y_ = j.minus(x_.mulScalar(new Fixed(129127208515966861312n))).floor(); // 7 + + let x = x_.mulScalar(ns_x).addScalar(ns_y); + let y = y_.mulScalar(ns_x).addScalar(ns_y); + let h = Vec4.splat(one).minus(x.abs()).minus(y.abs()); + + let b0 = Vec4.new(x.x, x.y, y.x, y.y); + let b1 = Vec4.new(x.z, x.w, y.z, y.w); + + // vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0; + // vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0; + let s0 = b0.floor().mulScalar(new Fixed(36893488147419103232n)).addScalar(one); + let s1 = b1.floor().mulScalar(new Fixed(36893488147419103232n)).addScalar(one); + + let sh = Vec4.new(step(h.x, zero).neg(), step(h.y, zero).neg(), step(h.z, zero).neg(), step(h.w, zero).neg()); + + let a0 = Vec4.new( + b0.x.add(s0.x.mul(sh.x)), + b0.z.add(s0.z.mul(sh.x)), + b0.y.add(s0.y.mul(sh.y)), + b0.w.add(s0.w.mul(sh.y)), + ); + let a1 = Vec4.new( + b1.x.add(s1.x.mul(sh.z)), + b1.z.add(s1.z.mul(sh.z)), + b1.y.add(s1.y.mul(sh.w)), + b1.w.add(s1.w.mul(sh.w)), + ); + + let p0 = Vec3.new(a0.x, a0.y, h.x); + let p1 = Vec3.new(a0.z, a0.w, h.y); + let p2 = Vec3.new(a1.x, a1.y, h.z); + let p3 = Vec3.new(a1.z, a1.w, h.w); + + let norm = taylor_inv_sqrt(Vec4.new(p0.dot(p0), p1.dot(p1), p2.dot(p2), p3.dot(p3))); + p0 = Vec3.new(p0.x.mul(norm.x), p0.y.mul(norm.x), p0.z.mul(norm.x)); + p1 = Vec3.new(p1.x.mul(norm.y), p1.y.mul(norm.y), p1.z.mul(norm.y)); + p2 = Vec3.new(p2.x.mul(norm.z), p2.y.mul(norm.z), p2.z.mul(norm.z)); + p3 = Vec3.new(p3.x.mul(norm.w), p3.y.mul(norm.w), p3.z.mul(norm.w)); + + let m = Vec4.new( + max(half.sub(x0.dot(x0)), zero), + max(half.sub(x1.dot(x1)), zero), + max(half.sub(x2.dot(x2)), zero), + max(half.sub(x3.dot(x3)), zero), + ); + + // m = (m * m) * (m * m); + m = m.mul(m).mul(m.mul(m)); + + let _105 = FixedTrait.fromInt(105n); + return _105.mul(m.dot(Vec4.new(p0.dot(x0), p1.dot(x1), p2.dot(x2), p3.dot(x3)))); +} diff --git a/packages/core/src/utils/biome/vec3.ts b/packages/core/src/utils/biome/vec3.ts new file mode 100644 index 000000000..0e9a68ab4 --- /dev/null +++ b/packages/core/src/utils/biome/vec3.ts @@ -0,0 +1,67 @@ +import { Fixed } from "./fixed-point"; +export class Vec3 { + constructor( + public x: Fixed, + public y: Fixed, + public z: Fixed, + ) {} + + static new(x: Fixed, y: Fixed, z: Fixed): Vec3 { + return new Vec3(x, y, z); + } + + static splat(v: Fixed): Vec3 { + return new Vec3(v, v, v); + } + + dot(other: Vec3): Fixed { + let x = this.x.mul(other.x); + let y = this.y.mul(other.y); + let z = this.z.mul(other.z); + return x.add(y).add(z); + } + + minus(other: Vec3): Vec3 { + return new Vec3(this.x.sub(other.x), this.y.sub(other.y), this.z.sub(other.z)); + } + + add(other: Vec3): Vec3 { + return new Vec3(this.x.add(other.x), this.y.add(other.y), this.z.add(other.z)); + } + + mul(other: Vec3): Vec3 { + return new Vec3(this.x.mul(other.x), this.y.mul(other.y), this.z.mul(other.z)); + } + + mod(other: Vec3): Vec3 { + return new Vec3(this.x.mod(other.x), this.y.mod(other.y), this.z.mod(other.z)); + } + + div(other: Vec3): Vec3 { + return new Vec3(this.x.div(other.x), this.y.div(other.y), this.z.div(other.z)); + } + + floor(): Vec3 { + return new Vec3(this.x.floor(), this.y.floor(), this.z.floor()); + } + + remScaler(scalar: Fixed): Vec3 { + return new Vec3(this.x.mod(scalar), this.y.mod(scalar), this.z.mod(scalar)); + } + + divScalar(scalar: Fixed): Vec3 { + return new Vec3(this.x.div(scalar), this.y.div(scalar), this.z.div(scalar)); + } + + mulScalar(scalar: Fixed): Vec3 { + return new Vec3(this.x.mul(scalar), this.y.mul(scalar), this.z.mul(scalar)); + } + + addScalar(scalar: Fixed): Vec3 { + return new Vec3(this.x.add(scalar), this.y.add(scalar), this.z.add(scalar)); + } + + abs(): Vec3 { + return new Vec3(this.x.abs(), this.y.abs(), this.z.abs()); + } +} diff --git a/packages/core/src/utils/biome/vec4.ts b/packages/core/src/utils/biome/vec4.ts new file mode 100644 index 000000000..818991b98 --- /dev/null +++ b/packages/core/src/utils/biome/vec4.ts @@ -0,0 +1,70 @@ +import { Fixed } from "./fixed-point"; + +export class Vec4 { + constructor( + public x: Fixed, + public y: Fixed, + public z: Fixed, + public w: Fixed, + ) {} + + static new(x: Fixed, y: Fixed, z: Fixed, w: Fixed): Vec4 { + return new Vec4(x, y, z, w); + } + + static splat(v: Fixed): Vec4 { + return new Vec4(v, v, v, v); + } + + dot(other: Vec4): Fixed { + let x = this.x.mul(other.x); + let y = this.y.mul(other.y); + let z = this.z.mul(other.z); + let w = this.w.mul(other.w); + return x.add(y).add(z).add(w); + } + + minus(other: Vec4): Vec4 { + return new Vec4(this.x.sub(other.x), this.y.sub(other.y), this.z.sub(other.z), this.w.sub(other.w)); + } + + add(other: Vec4): Vec4 { + return new Vec4(this.x.add(other.x), this.y.add(other.y), this.z.add(other.z), this.w.add(other.w)); + } + + mul(other: Vec4): Vec4 { + return new Vec4(this.x.mul(other.x), this.y.mul(other.y), this.z.mul(other.z), this.w.mul(other.w)); + } + + mod(other: Vec4): Vec4 { + return new Vec4(this.x.mod(other.x), this.y.mod(other.y), this.z.mod(other.z), this.w.mod(other.w)); + } + + div(other: Vec4): Vec4 { + return new Vec4(this.x.div(other.x), this.y.div(other.y), this.z.div(other.z), this.w.div(other.w)); + } + + floor(): Vec4 { + return new Vec4(this.x.floor(), this.y.floor(), this.z.floor(), this.w.floor()); + } + + remScaler(scalar: Fixed): Vec4 { + return new Vec4(this.x.mod(scalar), this.y.mod(scalar), this.z.mod(scalar), this.w.mod(scalar)); + } + + divScalar(scalar: Fixed): Vec4 { + return new Vec4(this.x.div(scalar), this.y.div(scalar), this.z.div(scalar), this.w.div(scalar)); + } + + mulScalar(scalar: Fixed): Vec4 { + return new Vec4(this.x.mul(scalar), this.y.mul(scalar), this.z.mul(scalar), this.w.mul(scalar)); + } + + addScalar(scalar: Fixed): Vec4 { + return new Vec4(this.x.add(scalar), this.y.add(scalar), this.z.add(scalar), this.w.add(scalar)); + } + + abs(): Vec4 { + return new Vec4(this.x.abs(), this.y.abs(), this.z.abs(), this.w.abs()); + } +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index ff4408fb0..d81952843 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,5 +1,6 @@ export * from "./army"; export * from "./battle-simulator"; +export * from "./biome"; export * from "./entities"; export * from "./guild"; export * from "./leaderboard"; @@ -10,4 +11,5 @@ export * from "./resources"; export * from "./structure"; export * from "./trades"; export * from "./transport"; +export * from "./travel-path"; export * from "./utils"; diff --git a/packages/core/src/utils/travel-path.ts b/packages/core/src/utils/travel-path.ts new file mode 100644 index 000000000..720c31bf7 --- /dev/null +++ b/packages/core/src/utils/travel-path.ts @@ -0,0 +1,51 @@ +import { FELT_CENTER } from "../constants"; +import { HexPosition } from "../types"; + +export class TravelPaths { + private readonly paths: Map; + + constructor() { + this.paths = new Map(); + } + + set(key: string, value: { path: HexPosition[]; isExplored: boolean }): void { + this.paths.set(key, value); + } + + deleteAll(): void { + this.paths.clear(); + } + + get(key: string): { path: HexPosition[]; isExplored: boolean } | undefined { + return this.paths.get(key); + } + + has(key: string): boolean { + return this.paths.has(key); + } + + values(): IterableIterator<{ path: HexPosition[]; isExplored: boolean }> { + return this.paths.values(); + } + + getHighlightedHexes(): Array<{ col: number; row: number }> { + return Array.from(this.paths.values()).map(({ path }) => ({ + col: path[path.length - 1].col - FELT_CENTER, + row: path[path.length - 1].row - FELT_CENTER, + })); + } + + isHighlighted(row: number, col: number): boolean { + return this.paths.has(TravelPaths.posKey({ col: col + FELT_CENTER, row: row + FELT_CENTER })); + } + + getPaths(): Map { + return this.paths; + } + + static posKey(pos: HexPosition, normalized = false): string { + const col = normalized ? pos.col + FELT_CENTER : pos.col; + const row = normalized ? pos.row + FELT_CENTER : pos.row; + return `${col},${row}`; + } +} From bcf247fef6f91bac3967cafcdc7559ac00feafe9 Mon Sep 17 00:00:00 2001 From: aymericdelab Date: Tue, 11 Feb 2025 18:31:22 +0100 Subject: [PATCH 02/10] fix find paths --- .../apps/game/src/three/scenes/worldmap.tsx | 47 +++++++++----- .../src/managers/army-movement-manager.ts | 63 +++++++++++++++++-- 2 files changed, 90 insertions(+), 20 deletions(-) diff --git a/client/apps/game/src/three/scenes/worldmap.tsx b/client/apps/game/src/three/scenes/worldmap.tsx index 4025d0bc4..a707ebda2 100644 --- a/client/apps/game/src/three/scenes/worldmap.tsx +++ b/client/apps/game/src/three/scenes/worldmap.tsx @@ -34,7 +34,7 @@ import throttle from "lodash/throttle"; import * as THREE from "three"; import { Raycaster } from "three"; import { MapControls } from "three/examples/jsm/controls/MapControls"; -import { ArmySystemUpdate, SceneName, TileSystemUpdate } from "../types"; +import { ArmySystemUpdate, SceneName, StructureSystemUpdate, TileSystemUpdate } from "../types"; import { getWorldPositionForHex } from "../utils"; export default class WorldmapScene extends HexagonScene { @@ -55,6 +55,7 @@ export default class WorldmapScene extends HexagonScene { private battleManager: BattleManager; private exploredTiles: Map> = new Map(); private armyHexes: Map> = new Map(); + private structureHexes: Map> = new Map(); private armiesPositions: Map = new Map(); private battles: Map> = new Map(); private tileManager: TileManager; @@ -130,6 +131,8 @@ export default class WorldmapScene extends HexagonScene { this.systemManager.Tile.onUpdate((value) => this.updateExploredHex(value)); this.systemManager.Army.onUpdate((value) => this.updateArmyHexes(value)); this.systemManager.Structure.onUpdate((value) => { + this.updateStructureHexes(value); + const optimisticStructure = this.structureManager.structures.removeStructure( Number(DUMMY_HYPERSTRUCTURE_ENTITY_ID), ); @@ -373,7 +376,10 @@ export default class WorldmapScene extends HexagonScene { const { currentDefaultTick, currentArmiesTick } = getBlockTimestamp(); const armyStamina = 10; + console.log("armies", this.armyHexes); + const travelPaths = armyMovementManager.findPaths( + this.structureHexes, this.armyHexes, this.exploredTiles, armyStamina, @@ -426,14 +432,34 @@ export default class WorldmapScene extends HexagonScene { const oldCol = oldHexCoords?.col; const oldRow = oldHexCoords?.row; - if (oldCol !== newCol || oldRow !== newRow) { - if (oldCol && oldRow) { + if (oldCol && oldRow) { + if (oldCol !== newCol || oldRow !== newRow) { this.armyHexes.get(oldCol)?.delete(oldRow); - } - if (newCol && newRow) { + if (!this.armyHexes.has(newCol)) { + this.armyHexes.set(newCol, new Set()); + } this.armyHexes.get(newCol)?.add(newRow); } + } else { + if (!this.armyHexes.has(newCol)) { + this.armyHexes.set(newCol, new Set()); + } + this.armyHexes.get(newCol)?.add(newRow); + } + } + + public updateStructureHexes(update: StructureSystemUpdate) { + const { + hexCoords: { col, row }, + } = update; + + const newCol = col - FELT_CENTER; + const newRow = row - FELT_CENTER; + + if (!this.structureHexes.has(newCol)) { + this.structureHexes.set(newCol, new Set()); } + this.structureHexes.get(newCol)?.add(newRow); } public async updateExploredHex(update: TileSystemUpdate) { @@ -546,16 +572,7 @@ export default class WorldmapScene extends HexagonScene { return chunks; } - removeCachedMatricesAroundColRow(col: number, row: number) { - for (let i = -this.renderChunkSize.width / 2; i <= this.renderChunkSize.width / 2; i += 10) { - for (let j = -this.renderChunkSize.width / 2; j <= this.renderChunkSize.height / 2; j += 10) { - if (i === 0 && j === 0) { - continue; - } - this.removeCachedMatricesForChunk(row + i, col + j); - } - } - } + removeCachedMatricesAroundColRow(col: number, row: number) {} clearCache() { this.cachedMatrices.clear(); diff --git a/packages/core/src/managers/army-movement-manager.ts b/packages/core/src/managers/army-movement-manager.ts index ca6855a12..322d57a88 100644 --- a/packages/core/src/managers/army-movement-manager.ts +++ b/packages/core/src/managers/army-movement-manager.ts @@ -181,16 +181,29 @@ export class ArmyMovementManager { } public findPaths( + structureHexes: Map>, armyHexes: Map>, exploredHexes: Map>, armyStamina: number, currentDefaultTick: number, currentArmiesTick: number, ): TravelPaths { + console.log("Finding paths with:", { + armyStamina, + currentDefaultTick, + currentArmiesTick, + }); + const startPos = this._getCurrentPosition(); const maxHex = this._calculateMaxTravelPossible(currentDefaultTick, currentArmiesTick); const canExplore = this._canExplore(currentDefaultTick, currentArmiesTick); + console.log("Initial conditions:", { + startPos, + maxHex, + canExplore, + }); + const priorityQueue: Array<{ position: HexPosition; staminaUsed: number; distance: number; path: HexPosition[] }> = [{ position: startPos, staminaUsed: 0, distance: 0, path: [startPos] }]; const travelPaths = new TravelPaths(); @@ -201,18 +214,36 @@ export class ArmyMovementManager { const { position: current, staminaUsed, distance, path } = priorityQueue.shift()!; const currentKey = TravelPaths.posKey(current); + console.log("Processing position:", { + current, + staminaUsed, + distance, + pathLength: path.length, + }); + if (!shortestDistances.has(currentKey) || distance < shortestDistances.get(currentKey)!) { shortestDistances.set(currentKey, distance); const isExplored = exploredHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; if (path.length >= 2) { travelPaths.set(currentKey, { path, isExplored }); } - if (!isExplored) continue; - const hasArmy = armyHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - console.log({ hasArmy }); - - if (hasArmy) continue; + // Skip army and explored checks for start position + if (path.length > 1) { + if (!isExplored) continue; + + const hasArmy = armyHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; + const hasStructure = structureHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; + console.log("Checking position:", { + col: current.col, + row: current.row, + isExplored, + hasArmy, + hasStructure, + }); + + if (hasArmy || hasStructure) continue; + } const neighbors = getNeighborHexes(current.col, current.row); for (const { col, row } of neighbors) { @@ -221,14 +252,36 @@ export class ArmyMovementManager { const nextPath = [...path, { col, row }]; const isExplored = exploredHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + const hasArmy = armyHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + const hasStructure = structureHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; const biome = exploredHexes.get(col - FELT_CENTER)?.get(row - FELT_CENTER); const staminaCost = biome ? ArmyMovementManager.staminaDrain(biome) : 0; const nextStaminaUsed = staminaUsed + staminaCost; + console.log("Evaluating neighbor:", { + col, + row, + hasStructure, + hasArmy, + isExplored, + biome, + staminaCost, + nextStaminaUsed, + }); + if (nextStaminaUsed > armyStamina) continue; + if (hasStructure) continue; + if (hasArmy) continue; if ((isExplored && nextDistance <= maxHex) || (!isExplored && canExplore && nextDistance === 1)) { if (!shortestDistances.has(neighborKey) || nextDistance < shortestDistances.get(neighborKey)!) { + console.log("Adding to priority queue:", { + col, + row, + nextDistance, + nextStaminaUsed, + }); + priorityQueue.push({ position: { col, row }, staminaUsed: nextStaminaUsed, From ca641d007f5476eab37ca2255d8f7d61b9d34e30 Mon Sep 17 00:00:00 2001 From: aymericdelab Date: Wed, 12 Feb 2025 10:33:36 +0100 Subject: [PATCH 03/10] fix pathfinding bug --- .../game/src/three/helpers/pathfinding.ts | 44 +++++++++++---- .../game/src/three/managers/army-manager.ts | 53 ++++++++---------- .../apps/game/src/three/scenes/worldmap.tsx | 2 +- .../src/managers/army-movement-manager.ts | 56 ++++++++++++++++--- 4 files changed, 105 insertions(+), 50 deletions(-) diff --git a/client/apps/game/src/three/helpers/pathfinding.ts b/client/apps/game/src/three/helpers/pathfinding.ts index 1bbf5f8ca..6de2afeb8 100644 --- a/client/apps/game/src/three/helpers/pathfinding.ts +++ b/client/apps/game/src/three/helpers/pathfinding.ts @@ -1,5 +1,5 @@ import { Position } from "@/types/position"; -import { getNeighborOffsets, HexPosition } from "@bibliothecadao/eternum"; +import { BiomeType, getNeighborOffsets, HexPosition } from "@bibliothecadao/eternum"; interface Node { col: number; @@ -10,17 +10,26 @@ interface Node { } export function findShortestPath( - startPosition: Position, - endPosition: Position, - exploredTiles: Map>, + oldPosition: Position, + newPosition: Position, + exploredTiles: Map>, + structureHexes: Map>, + armyHexes: Map>, + maxDistance: number, ): Position[] { + // Check if target is within maximum distance before starting pathfinding + const oldPos = oldPosition.getNormalized(); + const newPos = newPosition.getNormalized(); + const initialDistance = getHexDistance({ col: oldPos.x, row: oldPos.y }, { col: newPos.x, row: newPos.y }); + if (initialDistance > maxDistance) { + return []; // Return empty path if target is too far + } + const openSet: Node[] = []; const closedSet = new Set(); - const start = startPosition.getNormalized(); - const end = endPosition.getNormalized(); const startNode: Node = { - col: start.x, - row: start.y, + col: oldPos.x, + row: oldPos.y, f: 0, g: 0, }; @@ -32,7 +41,7 @@ export function findShortestPath( let current = openSet.reduce((min, node) => (node.f < min.f ? node : min), openSet[0]); // Reached end - if (current.col === end.x && current.row === end.y) { + if (current.col === newPos.x && current.row === newPos.y) { return reconstructPath(current); } @@ -46,13 +55,28 @@ export function findShortestPath( const neighborCol = current.col + i; const neighborRow = current.row + j; + // Skip if path is getting too long + if (current.g + 1 > maxDistance) { + continue; + } + // Skip if not explored or already in closed set if (!exploredTiles.get(neighborCol)?.has(neighborRow) || closedSet.has(`${neighborCol},${neighborRow}`)) { continue; } + // skip if the neighbor is a structure hex + if (structureHexes.get(neighborCol)?.has(neighborRow)) { + continue; + } + + // skip if the neighbor is an army hex and not the end hex (where the unit is going) + if (armyHexes.get(neighborCol)?.has(neighborRow) && !(neighborCol === newPos.x && neighborRow === newPos.y)) { + continue; + } + const g = current.g + 1; - const h = getHexDistance({ col: neighborCol, row: neighborRow }, { col: end.x, row: end.y }); + const h = getHexDistance({ col: neighborCol, row: neighborRow }, { col: newPos.x, row: newPos.y }); const f = g + h; const neighborNode: Node = { diff --git a/client/apps/game/src/three/managers/army-manager.ts b/client/apps/game/src/three/managers/army-manager.ts index 28a8b44c8..a5c4c9273 100644 --- a/client/apps/game/src/three/managers/army-manager.ts +++ b/client/apps/game/src/three/managers/army-manager.ts @@ -4,17 +4,10 @@ import { isAddressEqualToAccount } from "@/three/helpers/utils"; import { ArmyModel } from "@/three/managers/army-model"; import { LabelManager } from "@/three/managers/label-manager"; import { Position } from "@/types/position"; -import { - ArmyMovementManager, - Biome, - BiomeType, - ContractAddress, - FELT_CENTER, - ID, - orders, -} from "@bibliothecadao/eternum"; +import { Biome, BiomeType, ContractAddress, FELT_CENTER, ID, orders } from "@bibliothecadao/eternum"; import * as THREE from "three"; import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer"; +import { findShortestPath } from "../helpers/pathfinding"; import { ArmyData, ArmySystemUpdate, MovingArmyData, MovingLabelData, RenderChunkSize } from "../types"; import { calculateOffset, getHexForWorldPosition, getWorldPositionForHex } from "../utils"; @@ -128,7 +121,11 @@ export class ArmyManager { } } - async onUpdate(update: ArmySystemUpdate, armyHexes: Map>) { + async onUpdate( + update: ArmySystemUpdate, + armyHexes: Map>, + structureHexes: Map>, + ) { await this.armyModel.loadPromise; const { entityId, hexCoords, owner, battleId, currentHealth, order } = update; @@ -150,12 +147,12 @@ export class ArmyManager { } } - const position = new Position({ x: hexCoords.col, y: hexCoords.row }); + const newPosition = new Position({ x: hexCoords.col, y: hexCoords.row }); if (this.armies.has(entityId)) { - this.moveArmy(entityId, position, armyHexes); + this.moveArmy(entityId, newPosition, armyHexes, structureHexes); } else { - this.addArmy(entityId, position, owner, order); + this.addArmy(entityId, newPosition, owner, order); } return false; } @@ -307,30 +304,29 @@ export class ArmyManager { this.renderVisibleArmies(this.currentChunkKey!); } - public moveArmy(entityId: ID, hexCoords: Position, armyHexes: Map>) { + public moveArmy( + entityId: ID, + hexCoords: Position, + armyHexes: Map>, + structureHexes: Map>, + ) { const armyData = this.armies.get(entityId); if (!armyData) return; - const { x: startX, y: startY } = armyData.hexCoords.getNormalized(); - const { x: targetX, y: targetY } = hexCoords.getNormalized(); + const startPos = armyData.hexCoords.getNormalized(); + const targetPos = hexCoords.getNormalized(); - if (startX === targetX && startY === targetY) return; + if (startPos.x === targetPos.x && startPos.y === targetPos.y) return; - const path = ArmyMovementManager.findPath( - { col: armyData.hexCoords.getContract().x, row: armyData.hexCoords.getContract().y }, - { col: hexCoords.getContract().x, row: hexCoords.getContract().y }, - armyHexes, - this.exploredTiles, - ); - - // const path = findShortestPath(armyData.hexCoords, hexCoords, this.exploredTiles); + // todo: need to check better max distance + const path = findShortestPath(armyData.hexCoords, hexCoords, this.exploredTiles, structureHexes, armyHexes, 20); if (!path || path.length === 0) return; // Set initial direction before movement starts const firstHex = path[0]; const currentPosition = this.getArmyWorldPosition(entityId, armyData.hexCoords); - const newPosition = this.getArmyWorldPosition(entityId, new Position({ x: firstHex.col, y: firstHex.row })); + const newPosition = this.getArmyWorldPosition(entityId, firstHex); const direction = new THREE.Vector3().subVectors(newPosition, currentPosition).normalize(); const angle = Math.atan2(direction.x, direction.z); @@ -338,10 +334,7 @@ export class ArmyManager { // Update army position immediately to avoid starting from a "back" position this.armies.set(entityId, { ...armyData, hexCoords }); - this.armyPaths.set( - entityId, - path.map((hex) => new Position({ x: hex.col, y: hex.row })), - ); + this.armyPaths.set(entityId, path); const modelData = this.armyModel.getModelForEntity(entityId); if (modelData) { diff --git a/client/apps/game/src/three/scenes/worldmap.tsx b/client/apps/game/src/three/scenes/worldmap.tsx index a707ebda2..e5352f86a 100644 --- a/client/apps/game/src/three/scenes/worldmap.tsx +++ b/client/apps/game/src/three/scenes/worldmap.tsx @@ -120,7 +120,7 @@ export default class WorldmapScene extends HexagonScene { this.armySubscription?.unsubscribe(); this.armySubscription = this.systemManager.Army.onUpdate((update: ArmySystemUpdate) => { - this.armyManager.onUpdate(update, this.armyHexes).then((needsUpdate) => { + this.armyManager.onUpdate(update, this.armyHexes, this.structureHexes).then((needsUpdate) => { if (needsUpdate) { this.updateVisibleChunks(); } diff --git a/packages/core/src/managers/army-movement-manager.ts b/packages/core/src/managers/army-movement-manager.ts index 322d57a88..ce380261a 100644 --- a/packages/core/src/managers/army-movement-manager.ts +++ b/packages/core/src/managers/army-movement-manager.ts @@ -118,9 +118,15 @@ export class ArmyMovementManager { public static findPath( startPos: HexPosition, endPos: HexPosition, + structureHexes: Map>, armyHexes: Map>, exploredHexes: Map>, ): HexPosition[] { + console.log("[findPath] Finding path from", startPos, "to", endPos); + console.log("[findPath] Structure hexes:", structureHexes); + console.log("[findPath] Army hexes:", armyHexes); + console.log("[findPath] Explored hexes:", exploredHexes); + const priorityQueue: Array<{ position: HexPosition; staminaUsed: number; distance: number; path: HexPosition[] }> = [{ position: startPos, staminaUsed: 0, distance: 0, path: [startPos] }]; const shortestDistances = new Map(); @@ -130,7 +136,10 @@ export class ArmyMovementManager { const { position: current, staminaUsed, distance, path } = priorityQueue.shift()!; const currentKey = TravelPaths.posKey(current); + console.log("[findPath] Processing position:", current, "stamina:", staminaUsed, "distance:", distance); + if (current.col === endPos.col && current.row === endPos.row) { + console.log("[findPath] Found path:", path); return path; } @@ -142,22 +151,49 @@ export class ArmyMovementManager { ) { shortestDistances.set(currentKey, { distance, staminaUsed }); const isExplored = exploredHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - if (!isExplored) continue; - const hasArmy = armyHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - if (hasArmy) continue; + // Skip army and explored checks for start position + if (path.length > 1) { + if (!isExplored) { + console.log("[findPath] Skipping unexplored hex:", current); + continue; + } + + const hasArmy = armyHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; + const hasStructure = structureHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; + + if (hasArmy || hasStructure) { + console.log("[findPath] Skipping hex with army/structure:", current); + continue; + } + } const neighbors = getNeighborHexes(current.col, current.row); + console.log("[findPath] Checking neighbors:", neighbors); + for (const { col, row } of neighbors) { const neighborKey = TravelPaths.posKey({ col, row }); const nextDistance = distance + 1; const nextPath = [...path, { col, row }]; const isExplored = exploredHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + const hasArmy = armyHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + const hasStructure = structureHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; const biome = exploredHexes.get(col - FELT_CENTER)?.get(row - FELT_CENTER); const staminaCost = biome ? this.staminaDrain(biome) : 0; const nextStaminaUsed = staminaUsed + staminaCost; + console.log("[findPath] Neighbor:", { col, row }, "explored:", isExplored, "stamina cost:", staminaCost); + + if (hasStructure) { + console.log("[findPath] Skipping neighbor with structure:", { col, row }); + continue; + } + if (hasArmy) { + console.log("[findPath] Skipping neighbor with army:", { col, row }); + continue; + } + if (isExplored) { const shortest = shortestDistances.get(neighborKey); if ( @@ -165,6 +201,7 @@ export class ArmyMovementManager { nextStaminaUsed < shortest.staminaUsed || (nextStaminaUsed === shortest.staminaUsed && nextDistance < shortest.distance) ) { + console.log("[findPath] Adding neighbor to queue:", { col, row }); priorityQueue.push({ position: { col, row }, staminaUsed: nextStaminaUsed, @@ -177,6 +214,7 @@ export class ArmyMovementManager { } } + console.log("[findPath] No path found"); return []; } @@ -188,7 +226,7 @@ export class ArmyMovementManager { currentDefaultTick: number, currentArmiesTick: number, ): TravelPaths { - console.log("Finding paths with:", { + console.log("[findPaths] Finding paths with:", { armyStamina, currentDefaultTick, currentArmiesTick, @@ -198,7 +236,7 @@ export class ArmyMovementManager { const maxHex = this._calculateMaxTravelPossible(currentDefaultTick, currentArmiesTick); const canExplore = this._canExplore(currentDefaultTick, currentArmiesTick); - console.log("Initial conditions:", { + console.log("[findPaths] Initial conditions:", { startPos, maxHex, canExplore, @@ -214,7 +252,7 @@ export class ArmyMovementManager { const { position: current, staminaUsed, distance, path } = priorityQueue.shift()!; const currentKey = TravelPaths.posKey(current); - console.log("Processing position:", { + console.log("[findPaths] Processing position:", { current, staminaUsed, distance, @@ -234,7 +272,7 @@ export class ArmyMovementManager { const hasArmy = armyHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; const hasStructure = structureHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - console.log("Checking position:", { + console.log("[findPaths] Checking position:", { col: current.col, row: current.row, isExplored, @@ -258,7 +296,7 @@ export class ArmyMovementManager { const staminaCost = biome ? ArmyMovementManager.staminaDrain(biome) : 0; const nextStaminaUsed = staminaUsed + staminaCost; - console.log("Evaluating neighbor:", { + console.log("[findPaths] Evaluating neighbor:", { col, row, hasStructure, @@ -275,7 +313,7 @@ export class ArmyMovementManager { if ((isExplored && nextDistance <= maxHex) || (!isExplored && canExplore && nextDistance === 1)) { if (!shortestDistances.has(neighborKey) || nextDistance < shortestDistances.get(neighborKey)!) { - console.log("Adding to priority queue:", { + console.log("[findPaths] Adding to priority queue:", { col, row, nextDistance, From 941c7790a569e7418afc174b12006a6c15356b20 Mon Sep 17 00:00:00 2001 From: aymericdelab Date: Wed, 12 Feb 2025 11:18:15 +0100 Subject: [PATCH 04/10] fix old army position bug --- .../game/src/three/managers/army-manager.ts | 14 +- .../apps/game/src/three/scenes/worldmap.tsx | 8 +- .../src/managers/army-movement-manager.ts | 204 +++++++++--------- 3 files changed, 113 insertions(+), 113 deletions(-) diff --git a/client/apps/game/src/three/managers/army-manager.ts b/client/apps/game/src/three/managers/army-manager.ts index a5c4c9273..fa023f4d8 100644 --- a/client/apps/game/src/three/managers/army-manager.ts +++ b/client/apps/game/src/three/managers/army-manager.ts @@ -28,20 +28,14 @@ export class ArmyManager { private renderChunkSize: RenderChunkSize; private visibleArmies: ArmyData[] = []; private armyPaths: Map = new Map(); - private exploredTiles: Map>; private entityIdLabels: Map = new Map(); - constructor( - scene: THREE.Scene, - renderChunkSize: { width: number; height: number }, - exploredTiles: Map>, - ) { + constructor(scene: THREE.Scene, renderChunkSize: { width: number; height: number }) { this.scene = scene; this.armyModel = new ArmyModel(scene); this.scale = new THREE.Vector3(0.3, 0.3, 0.3); this.labelManager = new LabelManager("textures/army_label.png", 1.5); this.renderChunkSize = renderChunkSize; - this.exploredTiles = exploredTiles; this.onMouseMove = this.onMouseMove.bind(this); this.onRightClick = this.onRightClick.bind(this); @@ -125,6 +119,7 @@ export class ArmyManager { update: ArmySystemUpdate, armyHexes: Map>, structureHexes: Map>, + exploredTiles: Map>, ) { await this.armyModel.loadPromise; const { entityId, hexCoords, owner, battleId, currentHealth, order } = update; @@ -150,7 +145,7 @@ export class ArmyManager { const newPosition = new Position({ x: hexCoords.col, y: hexCoords.row }); if (this.armies.has(entityId)) { - this.moveArmy(entityId, newPosition, armyHexes, structureHexes); + this.moveArmy(entityId, newPosition, armyHexes, structureHexes, exploredTiles); } else { this.addArmy(entityId, newPosition, owner, order); } @@ -309,6 +304,7 @@ export class ArmyManager { hexCoords: Position, armyHexes: Map>, structureHexes: Map>, + exploredTiles: Map>, ) { const armyData = this.armies.get(entityId); if (!armyData) return; @@ -319,7 +315,7 @@ export class ArmyManager { if (startPos.x === targetPos.x && startPos.y === targetPos.y) return; // todo: need to check better max distance - const path = findShortestPath(armyData.hexCoords, hexCoords, this.exploredTiles, structureHexes, armyHexes, 20); + const path = findShortestPath(armyData.hexCoords, hexCoords, exploredTiles, structureHexes, armyHexes, 20); if (!path || path.length === 0) return; diff --git a/client/apps/game/src/three/scenes/worldmap.tsx b/client/apps/game/src/three/scenes/worldmap.tsx index e5352f86a..ef6ba0dd3 100644 --- a/client/apps/game/src/three/scenes/worldmap.tsx +++ b/client/apps/game/src/three/scenes/worldmap.tsx @@ -114,13 +114,13 @@ export default class WorldmapScene extends HexagonScene { }, ); - this.armyManager = new ArmyManager(this.scene, this.renderChunkSize, this.exploredTiles); + this.armyManager = new ArmyManager(this.scene, this.renderChunkSize); this.structureManager = new StructureManager(this.scene, this.renderChunkSize); this.battleManager = new BattleManager(this.scene); this.armySubscription?.unsubscribe(); this.armySubscription = this.systemManager.Army.onUpdate((update: ArmySystemUpdate) => { - this.armyManager.onUpdate(update, this.armyHexes, this.structureHexes).then((needsUpdate) => { + this.armyManager.onUpdate(update, this.armyHexes, this.structureHexes, this.exploredTiles).then((needsUpdate) => { if (needsUpdate) { this.updateVisibleChunks(); } @@ -431,6 +431,10 @@ export default class WorldmapScene extends HexagonScene { const oldHexCoords = this.armiesPositions.get(update.entityId); const oldCol = oldHexCoords?.col; const oldRow = oldHexCoords?.row; + console.log({ oldCol, oldRow, newCol, newRow }); + + // update the position of the army + this.armiesPositions.set(update.entityId, { col: newCol, row: newRow }); if (oldCol && oldRow) { if (oldCol !== newCol || oldRow !== newRow) { diff --git a/packages/core/src/managers/army-movement-manager.ts b/packages/core/src/managers/army-movement-manager.ts index ce380261a..17d51c182 100644 --- a/packages/core/src/managers/army-movement-manager.ts +++ b/packages/core/src/managers/army-movement-manager.ts @@ -115,108 +115,108 @@ export class ArmyMovementManager { return 0; } - public static findPath( - startPos: HexPosition, - endPos: HexPosition, - structureHexes: Map>, - armyHexes: Map>, - exploredHexes: Map>, - ): HexPosition[] { - console.log("[findPath] Finding path from", startPos, "to", endPos); - console.log("[findPath] Structure hexes:", structureHexes); - console.log("[findPath] Army hexes:", armyHexes); - console.log("[findPath] Explored hexes:", exploredHexes); - - const priorityQueue: Array<{ position: HexPosition; staminaUsed: number; distance: number; path: HexPosition[] }> = - [{ position: startPos, staminaUsed: 0, distance: 0, path: [startPos] }]; - const shortestDistances = new Map(); - - while (priorityQueue.length > 0) { - priorityQueue.sort((a, b) => a.staminaUsed - b.staminaUsed || a.distance - b.distance); - const { position: current, staminaUsed, distance, path } = priorityQueue.shift()!; - const currentKey = TravelPaths.posKey(current); - - console.log("[findPath] Processing position:", current, "stamina:", staminaUsed, "distance:", distance); - - if (current.col === endPos.col && current.row === endPos.row) { - console.log("[findPath] Found path:", path); - return path; - } - - const shortest = shortestDistances.get(currentKey); - if ( - !shortest || - staminaUsed < shortest.staminaUsed || - (staminaUsed === shortest.staminaUsed && distance < shortest.distance) - ) { - shortestDistances.set(currentKey, { distance, staminaUsed }); - const isExplored = exploredHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - - // Skip army and explored checks for start position - if (path.length > 1) { - if (!isExplored) { - console.log("[findPath] Skipping unexplored hex:", current); - continue; - } - - const hasArmy = armyHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - const hasStructure = structureHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - - if (hasArmy || hasStructure) { - console.log("[findPath] Skipping hex with army/structure:", current); - continue; - } - } - - const neighbors = getNeighborHexes(current.col, current.row); - console.log("[findPath] Checking neighbors:", neighbors); - - for (const { col, row } of neighbors) { - const neighborKey = TravelPaths.posKey({ col, row }); - const nextDistance = distance + 1; - const nextPath = [...path, { col, row }]; - - const isExplored = exploredHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; - const hasArmy = armyHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; - const hasStructure = structureHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; - const biome = exploredHexes.get(col - FELT_CENTER)?.get(row - FELT_CENTER); - const staminaCost = biome ? this.staminaDrain(biome) : 0; - const nextStaminaUsed = staminaUsed + staminaCost; - - console.log("[findPath] Neighbor:", { col, row }, "explored:", isExplored, "stamina cost:", staminaCost); - - if (hasStructure) { - console.log("[findPath] Skipping neighbor with structure:", { col, row }); - continue; - } - if (hasArmy) { - console.log("[findPath] Skipping neighbor with army:", { col, row }); - continue; - } - - if (isExplored) { - const shortest = shortestDistances.get(neighborKey); - if ( - !shortest || - nextStaminaUsed < shortest.staminaUsed || - (nextStaminaUsed === shortest.staminaUsed && nextDistance < shortest.distance) - ) { - console.log("[findPath] Adding neighbor to queue:", { col, row }); - priorityQueue.push({ - position: { col, row }, - staminaUsed: nextStaminaUsed, - distance: nextDistance, - path: nextPath, - }); - } - } - } - } - } - - console.log("[findPath] No path found"); - return []; - } + // public static findPath( + // startPos: HexPosition, + // endPos: HexPosition, + // structureHexes: Map>, + // armyHexes: Map>, + // exploredHexes: Map>, + // ): HexPosition[] { + // console.log("[findPath] Finding path from", startPos, "to", endPos); + // console.log("[findPath] Structure hexes:", structureHexes); + // console.log("[findPath] Army hexes:", armyHexes); + // console.log("[findPath] Explored hexes:", exploredHexes); + + // const priorityQueue: Array<{ position: HexPosition; staminaUsed: number; distance: number; path: HexPosition[] }> = + // [{ position: startPos, staminaUsed: 0, distance: 0, path: [startPos] }]; + // const shortestDistances = new Map(); + + // while (priorityQueue.length > 0) { + // priorityQueue.sort((a, b) => a.staminaUsed - b.staminaUsed || a.distance - b.distance); + // const { position: current, staminaUsed, distance, path } = priorityQueue.shift()!; + // const currentKey = TravelPaths.posKey(current); + + // console.log("[findPath] Processing position:", current, "stamina:", staminaUsed, "distance:", distance); + + // if (current.col === endPos.col && current.row === endPos.row) { + // console.log("[findPath] Found path:", path); + // return path; + // } + + // const shortest = shortestDistances.get(currentKey); + // if ( + // !shortest || + // staminaUsed < shortest.staminaUsed || + // (staminaUsed === shortest.staminaUsed && distance < shortest.distance) + // ) { + // shortestDistances.set(currentKey, { distance, staminaUsed }); + // const isExplored = exploredHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; + + // // Skip army and explored checks for start position + // if (path.length > 1) { + // if (!isExplored) { + // console.log("[findPath] Skipping unexplored hex:", current); + // continue; + // } + + // const hasArmy = armyHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; + // const hasStructure = structureHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; + + // if (hasArmy || hasStructure) { + // console.log("[findPath] Skipping hex with army/structure:", current); + // continue; + // } + // } + + // const neighbors = getNeighborHexes(current.col, current.row); + // console.log("[findPath] Checking neighbors:", neighbors); + + // for (const { col, row } of neighbors) { + // const neighborKey = TravelPaths.posKey({ col, row }); + // const nextDistance = distance + 1; + // const nextPath = [...path, { col, row }]; + + // const isExplored = exploredHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + // const hasArmy = armyHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + // const hasStructure = structureHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + // const biome = exploredHexes.get(col - FELT_CENTER)?.get(row - FELT_CENTER); + // const staminaCost = biome ? this.staminaDrain(biome) : 0; + // const nextStaminaUsed = staminaUsed + staminaCost; + + // console.log("[findPath] Neighbor:", { col, row }, "explored:", isExplored, "stamina cost:", staminaCost); + + // if (hasStructure) { + // console.log("[findPath] Skipping neighbor with structure:", { col, row }); + // continue; + // } + // if (hasArmy) { + // console.log("[findPath] Skipping neighbor with army:", { col, row }); + // continue; + // } + + // if (isExplored) { + // const shortest = shortestDistances.get(neighborKey); + // if ( + // !shortest || + // nextStaminaUsed < shortest.staminaUsed || + // (nextStaminaUsed === shortest.staminaUsed && nextDistance < shortest.distance) + // ) { + // console.log("[findPath] Adding neighbor to queue:", { col, row }); + // priorityQueue.push({ + // position: { col, row }, + // staminaUsed: nextStaminaUsed, + // distance: nextDistance, + // path: nextPath, + // }); + // } + // } + // } + // } + // } + + // console.log("[findPath] No path found"); + // return []; + // } public findPaths( structureHexes: Map>, From 62145670c31b57205e15128631682c4f8ba557b8 Mon Sep 17 00:00:00 2001 From: aymericdelab Date: Wed, 12 Feb 2025 12:17:32 +0100 Subject: [PATCH 05/10] remove felt center from client --- client/apps/game/src/three/scenes/worldmap.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/client/apps/game/src/three/scenes/worldmap.tsx b/client/apps/game/src/three/scenes/worldmap.tsx index ef6ba0dd3..1db28ed5c 100644 --- a/client/apps/game/src/three/scenes/worldmap.tsx +++ b/client/apps/game/src/three/scenes/worldmap.tsx @@ -56,6 +56,7 @@ export default class WorldmapScene extends HexagonScene { private exploredTiles: Map> = new Map(); private armyHexes: Map> = new Map(); private structureHexes: Map> = new Map(); + // normalized hex positions private armiesPositions: Map = new Map(); private battles: Map> = new Map(); private tileManager: TileManager; @@ -426,8 +427,9 @@ export default class WorldmapScene extends HexagonScene { const { hexCoords: { col, row }, } = update; - const newCol = col - FELT_CENTER; - const newRow = row - FELT_CENTER; + const normalized = new Position({ x: col, y: row }).getNormalized(); + const newCol = normalized.x; + const newRow = normalized.y; const oldHexCoords = this.armiesPositions.get(update.entityId); const oldCol = oldHexCoords?.col; const oldRow = oldHexCoords?.row; @@ -457,8 +459,10 @@ export default class WorldmapScene extends HexagonScene { hexCoords: { col, row }, } = update; - const newCol = col - FELT_CENTER; - const newRow = row - FELT_CENTER; + const normalized = new Position({ x: col, y: row }).getNormalized(); + + const newCol = normalized.x; + const newRow = normalized.y; if (!this.structureHexes.has(newCol)) { this.structureHexes.set(newCol, new Set()); @@ -469,8 +473,10 @@ export default class WorldmapScene extends HexagonScene { public async updateExploredHex(update: TileSystemUpdate) { const { hexCoords, removeExplored, biome } = update; - const col = hexCoords.col - FELT_CENTER; - const row = hexCoords.row - FELT_CENTER; + const normalized = new Position({ x: hexCoords.col, y: hexCoords.row }).getNormalized(); + + const col = normalized.x; + const row = normalized.y; if (removeExplored) { const chunkRow = parseInt(this.currentChunk.split(",")[0]); From 5dda1060f259bdd16fe7928d6e4f9e7250393d13 Mon Sep 17 00:00:00 2001 From: aymericdelab Date: Wed, 12 Feb 2025 12:17:49 +0100 Subject: [PATCH 06/10] remove comment --- .../src/managers/army-movement-manager.ts | 103 ------------------ 1 file changed, 103 deletions(-) diff --git a/packages/core/src/managers/army-movement-manager.ts b/packages/core/src/managers/army-movement-manager.ts index 17d51c182..39ade2755 100644 --- a/packages/core/src/managers/army-movement-manager.ts +++ b/packages/core/src/managers/army-movement-manager.ts @@ -115,109 +115,6 @@ export class ArmyMovementManager { return 0; } - // public static findPath( - // startPos: HexPosition, - // endPos: HexPosition, - // structureHexes: Map>, - // armyHexes: Map>, - // exploredHexes: Map>, - // ): HexPosition[] { - // console.log("[findPath] Finding path from", startPos, "to", endPos); - // console.log("[findPath] Structure hexes:", structureHexes); - // console.log("[findPath] Army hexes:", armyHexes); - // console.log("[findPath] Explored hexes:", exploredHexes); - - // const priorityQueue: Array<{ position: HexPosition; staminaUsed: number; distance: number; path: HexPosition[] }> = - // [{ position: startPos, staminaUsed: 0, distance: 0, path: [startPos] }]; - // const shortestDistances = new Map(); - - // while (priorityQueue.length > 0) { - // priorityQueue.sort((a, b) => a.staminaUsed - b.staminaUsed || a.distance - b.distance); - // const { position: current, staminaUsed, distance, path } = priorityQueue.shift()!; - // const currentKey = TravelPaths.posKey(current); - - // console.log("[findPath] Processing position:", current, "stamina:", staminaUsed, "distance:", distance); - - // if (current.col === endPos.col && current.row === endPos.row) { - // console.log("[findPath] Found path:", path); - // return path; - // } - - // const shortest = shortestDistances.get(currentKey); - // if ( - // !shortest || - // staminaUsed < shortest.staminaUsed || - // (staminaUsed === shortest.staminaUsed && distance < shortest.distance) - // ) { - // shortestDistances.set(currentKey, { distance, staminaUsed }); - // const isExplored = exploredHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - - // // Skip army and explored checks for start position - // if (path.length > 1) { - // if (!isExplored) { - // console.log("[findPath] Skipping unexplored hex:", current); - // continue; - // } - - // const hasArmy = armyHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - // const hasStructure = structureHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - - // if (hasArmy || hasStructure) { - // console.log("[findPath] Skipping hex with army/structure:", current); - // continue; - // } - // } - - // const neighbors = getNeighborHexes(current.col, current.row); - // console.log("[findPath] Checking neighbors:", neighbors); - - // for (const { col, row } of neighbors) { - // const neighborKey = TravelPaths.posKey({ col, row }); - // const nextDistance = distance + 1; - // const nextPath = [...path, { col, row }]; - - // const isExplored = exploredHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; - // const hasArmy = armyHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; - // const hasStructure = structureHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; - // const biome = exploredHexes.get(col - FELT_CENTER)?.get(row - FELT_CENTER); - // const staminaCost = biome ? this.staminaDrain(biome) : 0; - // const nextStaminaUsed = staminaUsed + staminaCost; - - // console.log("[findPath] Neighbor:", { col, row }, "explored:", isExplored, "stamina cost:", staminaCost); - - // if (hasStructure) { - // console.log("[findPath] Skipping neighbor with structure:", { col, row }); - // continue; - // } - // if (hasArmy) { - // console.log("[findPath] Skipping neighbor with army:", { col, row }); - // continue; - // } - - // if (isExplored) { - // const shortest = shortestDistances.get(neighborKey); - // if ( - // !shortest || - // nextStaminaUsed < shortest.staminaUsed || - // (nextStaminaUsed === shortest.staminaUsed && nextDistance < shortest.distance) - // ) { - // console.log("[findPath] Adding neighbor to queue:", { col, row }); - // priorityQueue.push({ - // position: { col, row }, - // staminaUsed: nextStaminaUsed, - // distance: nextDistance, - // path: nextPath, - // }); - // } - // } - // } - // } - // } - - // console.log("[findPath] No path found"); - // return []; - // } - public findPaths( structureHexes: Map>, armyHexes: Map>, From a931353c3e375e335dca26c524bca081fba3fb0f Mon Sep 17 00:00:00 2001 From: aymericdelab Date: Wed, 12 Feb 2025 18:13:40 +0100 Subject: [PATCH 07/10] fix exploration bug + add biome path --- .../apps/game/src/hooks/store/_three-store.ts | 4 +- .../apps/game/src/three/scenes/worldmap.tsx | 6 +- .../worldmap/armies/action-info.tsx | 13 ++- .../src/ui/elements/stamina-resource-cost.tsx | 48 ++++++--- .../src/managers/army-movement-manager.ts | 100 ++++++++++++++---- packages/core/src/types/common.ts | 14 +++ packages/core/src/utils/travel-path.ts | 12 +-- 7 files changed, 146 insertions(+), 51 deletions(-) diff --git a/client/apps/game/src/hooks/store/_three-store.ts b/client/apps/game/src/hooks/store/_three-store.ts index eecad2d02..7ec0178e3 100644 --- a/client/apps/game/src/hooks/store/_three-store.ts +++ b/client/apps/game/src/hooks/store/_three-store.ts @@ -1,5 +1,5 @@ import { StructureInfo } from "@/three/types"; -import { BuildingType, HexPosition, ID, Position } from "@bibliothecadao/eternum"; +import { BuildingType, HexPosition, HexTileInfo, ID, Position } from "@bibliothecadao/eternum"; export interface ThreeStore { navigationTarget: HexPosition | null; @@ -35,7 +35,7 @@ export interface ThreeStore { interface ArmyActions { hoveredHex: HexPosition | null; - travelPaths: Map; + travelPaths: Map; selectedEntityId: ID | null; } diff --git a/client/apps/game/src/three/scenes/worldmap.tsx b/client/apps/game/src/three/scenes/worldmap.tsx index 1db28ed5c..1991993b3 100644 --- a/client/apps/game/src/three/scenes/worldmap.tsx +++ b/client/apps/game/src/three/scenes/worldmap.tsx @@ -25,6 +25,7 @@ import { HexPosition, ID, SetupResult, + StaminaManager, TileManager, TravelPaths, getNeighborOffsets, @@ -375,15 +376,14 @@ export default class WorldmapScene extends HexagonScene { ); const { currentDefaultTick, currentArmiesTick } = getBlockTimestamp(); - const armyStamina = 10; - console.log("armies", this.armyHexes); + const stamina = new StaminaManager(this.dojo.components, selectedEntityId).getStamina(currentArmiesTick).amount; const travelPaths = armyMovementManager.findPaths( this.structureHexes, this.armyHexes, this.exploredTiles, - armyStamina, + stamina, currentDefaultTick, currentArmiesTick, ); diff --git a/client/apps/game/src/ui/components/worldmap/armies/action-info.tsx b/client/apps/game/src/ui/components/worldmap/armies/action-info.tsx index 280e44c25..014605ab4 100644 --- a/client/apps/game/src/ui/components/worldmap/armies/action-info.tsx +++ b/client/apps/game/src/ui/components/worldmap/armies/action-info.tsx @@ -10,13 +10,14 @@ import { computeTravelFoodCosts, configManager, getBalance, + HexTileInfo, ID, ResourcesIds, } from "@bibliothecadao/eternum"; import { useDojo } from "@bibliothecadao/react"; import { getComponentValue } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; -import { memo, useCallback, useMemo } from "react"; +import { memo, useCallback, useEffect, useMemo } from "react"; const TooltipContent = memo( ({ @@ -28,7 +29,7 @@ const TooltipContent = memo( getBalance, }: { isExplored: boolean; - travelPath: any; + travelPath: { path: HexTileInfo[]; isExplored: boolean }; costs: { travelFoodCosts: any; exploreFoodCosts: any }; selectedEntityId: number; structureEntityId: number; @@ -66,7 +67,7 @@ const TooltipContent = memo( {!isExplored && (
@@ -117,6 +118,10 @@ export const ActionInfo = memo(() => { .armyActions.travelPaths.get(`${hoveredHex.col + FELT_CENTER},${hoveredHex.row + FELT_CENTER}`); }, [hoveredHex]); + useEffect(() => { + console.log({ travelPath }); + }, [travelPath]); + const showTooltip = useMemo(() => { return travelPath !== undefined && travelPath.path.length >= 2 && selectedEntityId !== null; }, [travelPath, selectedEntityId]); @@ -131,7 +136,7 @@ export const ActionInfo = memo(() => { [selectedEntityTroops], ); - if (!showTooltip || !selectedEntityId) return null; + if (!showTooltip || !selectedEntityId || !travelPath) return null; return ( diff --git a/client/apps/game/src/ui/elements/stamina-resource-cost.tsx b/client/apps/game/src/ui/elements/stamina-resource-cost.tsx index 0bab65a1a..c4bc8d6e5 100644 --- a/client/apps/game/src/ui/elements/stamina-resource-cost.tsx +++ b/client/apps/game/src/ui/elements/stamina-resource-cost.tsx @@ -1,41 +1,63 @@ import { useBlockTimestamp } from "@/hooks/helpers/use-block-timestamp"; -import { configManager, ID } from "@bibliothecadao/eternum"; +import { configManager, HexTileInfo, ID } from "@bibliothecadao/eternum"; import { useStaminaManager } from "@bibliothecadao/react"; import clsx from "clsx"; import { useMemo } from "react"; export const StaminaResourceCost = ({ travelingEntityId, - travelLength, isExplored, + path, }: { travelingEntityId: ID | undefined; - travelLength: number; isExplored: boolean; + path: HexTileInfo[]; }) => { const { currentArmiesTick } = useBlockTimestamp(); const staminaManager = useStaminaManager(travelingEntityId || 0); const stamina = useMemo(() => staminaManager.getStamina(currentArmiesTick), [currentArmiesTick, staminaManager]); - const destinationHex = useMemo(() => { + const pathInfo = useMemo(() => { if (!stamina) return; - const costs = - travelLength * (isExplored ? -configManager.getTravelStaminaCost() : -configManager.getExploreStaminaCost()); - const balanceColor = stamina !== undefined && stamina.amount < costs ? "text-red/90" : "text-green/90"; - return { isExplored, costs, balanceColor, balance: stamina.amount }; - }, [stamina, travelLength]); + + // Calculate total cost and collect biome info + const totalCost = path.reduce((acc, tile) => { + return acc + tile.staminaCost; + }, 0); + + const biomeInfo = path.map((tile) => ({ + biomeType: tile.biomeType, + cost: isExplored ? -tile.staminaCost : -configManager.getExploreStaminaCost() * tile.staminaCost, + })); + + const balanceColor = stamina.amount < totalCost ? "text-order-giants" : "text-order-brilliance"; + + return { + isExplored, + totalCost: isExplored ? -totalCost : -configManager.getExploreStaminaCost(), + biomeInfo, + balanceColor, + balance: stamina.amount, + }; + }, [stamina, path, isExplored]); return ( - destinationHex && ( + pathInfo && (
⚡️
- {destinationHex?.costs}{" "} - ({destinationHex.balance}) + {pathInfo.totalCost}{" "} + ({pathInfo.balance}) +
+
+ {pathInfo.biomeInfo.map((biome, index) => ( +
+ {biome.biomeType}: {biome.cost} +
+ ))}
-
Stamina
) diff --git a/packages/core/src/managers/army-movement-manager.ts b/packages/core/src/managers/army-movement-manager.ts index 39ade2755..a0330c5e1 100644 --- a/packages/core/src/managers/army-movement-manager.ts +++ b/packages/core/src/managers/army-movement-manager.ts @@ -13,7 +13,7 @@ import { } from "../constants"; import { ClientComponents } from "../dojo/create-client-components"; import { EternumProvider } from "../provider"; -import { ContractAddress, HexPosition, ID, TravelTypes } from "../types"; +import { ContractAddress, HexPosition, HexTileInfo, ID, TravelTypes, TroopType } from "../types"; import { Biome, multiplyByPrecision } from "../utils"; import { TravelPaths } from "../utils/travel-path"; import { configManager } from "./config-manager"; @@ -41,6 +41,21 @@ export class ArmyMovementManager { this.staminaManager = new StaminaManager(this.components, entityId); } + private _getTroopType(): TroopType { + const entityArmy = getComponentValue(this.components.Army, this.entity); + const knightCount = entityArmy?.troops?.knight_count ?? 0; + const crossbowmanCount = entityArmy?.troops?.crossbowman_count ?? 0; + const paladinCount = entityArmy?.troops?.paladin_count ?? 0; + + if (knightCount >= crossbowmanCount && knightCount >= paladinCount) { + return TroopType.Knight; + } + if (crossbowmanCount >= knightCount && crossbowmanCount >= paladinCount) { + return TroopType.Crossbowman; + } + return TroopType.Paladin; + } + private _canExplore(currentDefaultTick: number, currentArmiesTick: number): boolean { const stamina = this.staminaManager.getStamina(currentArmiesTick); @@ -99,20 +114,46 @@ export class ArmyMovementManager { }; } - public static staminaDrain(biome: BiomeType) { - if (biome === BiomeType.Grassland) { - return 1; + public static staminaDrain(biome: BiomeType, troopType: TroopType) { + const baseStaminaCost = 20; // Base cost to move to adjacent hex + + // Biome-specific modifiers per troop type + switch (biome) { + case BiomeType.Ocean: + return baseStaminaCost - 10; // -10 for all troops + case BiomeType.DeepOcean: + return baseStaminaCost - 10; // -10 for all troops + case BiomeType.Beach: + return baseStaminaCost; // No modifier + case BiomeType.Grassland: + return baseStaminaCost + (troopType === TroopType.Paladin ? -10 : 0); + case BiomeType.Shrubland: + return baseStaminaCost + (troopType === TroopType.Paladin ? -10 : 0); + case BiomeType.SubtropicalDesert: + return baseStaminaCost + (troopType === TroopType.Paladin ? -10 : 0); + case BiomeType.TemperateDesert: + return baseStaminaCost + (troopType === TroopType.Paladin ? -10 : 0); + case BiomeType.TropicalRainForest: + return baseStaminaCost + (troopType === TroopType.Paladin ? 10 : 0); + case BiomeType.TropicalSeasonalForest: + return baseStaminaCost + (troopType === TroopType.Paladin ? 10 : 0); + case BiomeType.TemperateRainForest: + return baseStaminaCost + (troopType === TroopType.Paladin ? 10 : 0); + case BiomeType.TemperateDeciduousForest: + return baseStaminaCost + (troopType === TroopType.Paladin ? 10 : 0); + case BiomeType.Tundra: + return baseStaminaCost + (troopType === TroopType.Paladin ? -10 : 0); + case BiomeType.Taiga: + return baseStaminaCost + (troopType === TroopType.Paladin ? 10 : 0); + case BiomeType.Snow: + return baseStaminaCost + (troopType === TroopType.Paladin ? 0 : 10); + case BiomeType.Bare: + return baseStaminaCost + (troopType === TroopType.Paladin ? -10 : 0); + case BiomeType.Scorched: + return baseStaminaCost + 10; // +10 for all troops + default: + return baseStaminaCost; } - if (biome === BiomeType.Bare) { - return 2; - } - if (biome === BiomeType.Snow) { - return 3; - } - if (biome === BiomeType.Tundra) { - return 4; - } - return 0; } public findPaths( @@ -123,7 +164,10 @@ export class ArmyMovementManager { currentDefaultTick: number, currentArmiesTick: number, ): TravelPaths { + const troopType = this._getTroopType(); + console.log("[findPaths] Finding paths with:", { + troopType, armyStamina, currentDefaultTick, currentArmiesTick, @@ -139,13 +183,20 @@ export class ArmyMovementManager { canExplore, }); - const priorityQueue: Array<{ position: HexPosition; staminaUsed: number; distance: number; path: HexPosition[] }> = - [{ position: startPos, staminaUsed: 0, distance: 0, path: [startPos] }]; + const priorityQueue: Array<{ position: HexPosition; staminaUsed: number; distance: number; path: HexTileInfo[] }> = + [ + { + position: startPos, + staminaUsed: 0, + distance: 0, + path: [{ col: startPos.col, row: startPos.row, biomeType: BiomeType.Grassland, staminaCost: 0 }], + }, + ]; const travelPaths = new TravelPaths(); - const shortestDistances = new Map(); + const lowestStaminaUse = new Map(); while (priorityQueue.length > 0) { - priorityQueue.sort((a, b) => a.distance - b.distance); + priorityQueue.sort((a, b) => a.staminaUsed - b.staminaUsed); const { position: current, staminaUsed, distance, path } = priorityQueue.shift()!; const currentKey = TravelPaths.posKey(current); @@ -156,8 +207,8 @@ export class ArmyMovementManager { pathLength: path.length, }); - if (!shortestDistances.has(currentKey) || distance < shortestDistances.get(currentKey)!) { - shortestDistances.set(currentKey, distance); + if (!lowestStaminaUse.has(currentKey) || staminaUsed < lowestStaminaUse.get(currentKey)!) { + lowestStaminaUse.set(currentKey, staminaUsed); const isExplored = exploredHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; if (path.length >= 2) { travelPaths.set(currentKey, { path, isExplored }); @@ -184,14 +235,16 @@ export class ArmyMovementManager { for (const { col, row } of neighbors) { const neighborKey = TravelPaths.posKey({ col, row }); const nextDistance = distance + 1; - const nextPath = [...path, { col, row }]; const isExplored = exploredHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; const hasArmy = armyHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; const hasStructure = structureHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; const biome = exploredHexes.get(col - FELT_CENTER)?.get(row - FELT_CENTER); - const staminaCost = biome ? ArmyMovementManager.staminaDrain(biome) : 0; + + const EXPLORATION_STAMINA_COST = configManager.getExploreStaminaCost(); + const staminaCost = biome ? ArmyMovementManager.staminaDrain(biome, troopType) : EXPLORATION_STAMINA_COST; const nextStaminaUsed = staminaUsed + staminaCost; + const nextPath = [...path, { col, row, biomeType: biome, staminaCost: staminaCost }]; console.log("[findPaths] Evaluating neighbor:", { col, @@ -204,12 +257,13 @@ export class ArmyMovementManager { nextStaminaUsed, }); + // if (!biome) continue; if (nextStaminaUsed > armyStamina) continue; if (hasStructure) continue; if (hasArmy) continue; if ((isExplored && nextDistance <= maxHex) || (!isExplored && canExplore && nextDistance === 1)) { - if (!shortestDistances.has(neighborKey) || nextDistance < shortestDistances.get(neighborKey)!) { + if (!lowestStaminaUse.has(neighborKey) || nextStaminaUsed < lowestStaminaUse.get(neighborKey)!) { console.log("[findPaths] Adding to priority queue:", { col, row, diff --git a/packages/core/src/types/common.ts b/packages/core/src/types/common.ts index ce5f91357..8654bcce1 100644 --- a/packages/core/src/types/common.ts +++ b/packages/core/src/types/common.ts @@ -1,6 +1,7 @@ import { ComponentValue, Entity } from "@dojoengine/recs"; import { Account, AccountInterface } from "starknet"; import { + BiomeType, BuildingType, CapacityConfigCategory, QuestType, @@ -162,6 +163,13 @@ export enum ClaimStatus { export type HexPosition = { col: number; row: number }; +export type HexTileInfo = { + col: number; + row: number; + staminaCost: number; + biomeType: BiomeType | undefined; +}; + export enum Winner { Attacker = "Attacker", Target = "Target", @@ -207,6 +215,12 @@ export interface Health { lifetime: bigint; } +export enum TroopType { + Knight = "Knight", + Crossbowman = "Crossbowman", + Paladin = "Paladin", +} + export interface CombatResultInterface { attackerRealmEntityId: ID; targetRealmEntityId: ID; diff --git a/packages/core/src/utils/travel-path.ts b/packages/core/src/utils/travel-path.ts index 720c31bf7..6272b067d 100644 --- a/packages/core/src/utils/travel-path.ts +++ b/packages/core/src/utils/travel-path.ts @@ -1,14 +1,14 @@ import { FELT_CENTER } from "../constants"; -import { HexPosition } from "../types"; +import { HexPosition, HexTileInfo } from "../types"; export class TravelPaths { - private readonly paths: Map; + private readonly paths: Map; constructor() { this.paths = new Map(); } - set(key: string, value: { path: HexPosition[]; isExplored: boolean }): void { + set(key: string, value: { path: HexTileInfo[]; isExplored: boolean }): void { this.paths.set(key, value); } @@ -16,7 +16,7 @@ export class TravelPaths { this.paths.clear(); } - get(key: string): { path: HexPosition[]; isExplored: boolean } | undefined { + get(key: string): { path: HexTileInfo[]; isExplored: boolean } | undefined { return this.paths.get(key); } @@ -24,7 +24,7 @@ export class TravelPaths { return this.paths.has(key); } - values(): IterableIterator<{ path: HexPosition[]; isExplored: boolean }> { + values(): IterableIterator<{ path: HexTileInfo[]; isExplored: boolean }> { return this.paths.values(); } @@ -39,7 +39,7 @@ export class TravelPaths { return this.paths.has(TravelPaths.posKey({ col: col + FELT_CENTER, row: row + FELT_CENTER })); } - getPaths(): Map { + getPaths(): Map { return this.paths; } From ba75b5c5366f6c9807c3632a7e6404a9786748c4 Mon Sep 17 00:00:00 2001 From: aymericdelab Date: Wed, 12 Feb 2025 18:31:12 +0100 Subject: [PATCH 08/10] fix action info and simplify findPaths --- .../worldmap/armies/action-info.tsx | 8 +- .../src/ui/elements/stamina-resource-cost.tsx | 24 ++-- .../src/managers/army-movement-manager.ts | 125 +++++++----------- 3 files changed, 60 insertions(+), 97 deletions(-) diff --git a/client/apps/game/src/ui/components/worldmap/armies/action-info.tsx b/client/apps/game/src/ui/components/worldmap/armies/action-info.tsx index 014605ab4..663e97491 100644 --- a/client/apps/game/src/ui/components/worldmap/armies/action-info.tsx +++ b/client/apps/game/src/ui/components/worldmap/armies/action-info.tsx @@ -17,7 +17,7 @@ import { import { useDojo } from "@bibliothecadao/react"; import { getComponentValue } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; -import { memo, useCallback, useEffect, useMemo } from "react"; +import { memo, useCallback, useMemo } from "react"; const TooltipContent = memo( ({ @@ -67,7 +67,7 @@ const TooltipContent = memo( {!isExplored && (
@@ -118,10 +118,6 @@ export const ActionInfo = memo(() => { .armyActions.travelPaths.get(`${hoveredHex.col + FELT_CENTER},${hoveredHex.row + FELT_CENTER}`); }, [hoveredHex]); - useEffect(() => { - console.log({ travelPath }); - }, [travelPath]); - const showTooltip = useMemo(() => { return travelPath !== undefined && travelPath.path.length >= 2 && selectedEntityId !== null; }, [travelPath, selectedEntityId]); diff --git a/client/apps/game/src/ui/elements/stamina-resource-cost.tsx b/client/apps/game/src/ui/elements/stamina-resource-cost.tsx index c4bc8d6e5..68bc3d54e 100644 --- a/client/apps/game/src/ui/elements/stamina-resource-cost.tsx +++ b/client/apps/game/src/ui/elements/stamina-resource-cost.tsx @@ -26,17 +26,11 @@ export const StaminaResourceCost = ({ return acc + tile.staminaCost; }, 0); - const biomeInfo = path.map((tile) => ({ - biomeType: tile.biomeType, - cost: isExplored ? -tile.staminaCost : -configManager.getExploreStaminaCost() * tile.staminaCost, - })); - const balanceColor = stamina.amount < totalCost ? "text-order-giants" : "text-order-brilliance"; return { isExplored, - totalCost: isExplored ? -totalCost : -configManager.getExploreStaminaCost(), - biomeInfo, + totalCost, balanceColor, balance: stamina.amount, }; @@ -48,15 +42,19 @@ export const StaminaResourceCost = ({
⚡️
- {pathInfo.totalCost}{" "} + {pathInfo.isExplored ? pathInfo.totalCost : configManager.getExploreStaminaCost()}{" "} ({pathInfo.balance})
- {pathInfo.biomeInfo.map((biome, index) => ( -
- {biome.biomeType}: {biome.cost} -
- ))} + {pathInfo.isExplored ? ( + path.map((tile, index) => ( +
+ {tile.biomeType}: {tile.staminaCost} +
+ )) + ) : ( +
Exploration
+ )}
diff --git a/packages/core/src/managers/army-movement-manager.ts b/packages/core/src/managers/army-movement-manager.ts index a0330c5e1..6c3f6b1b8 100644 --- a/packages/core/src/managers/army-movement-manager.ts +++ b/packages/core/src/managers/army-movement-manager.ts @@ -165,14 +165,6 @@ export class ArmyMovementManager { currentArmiesTick: number, ): TravelPaths { const troopType = this._getTroopType(); - - console.log("[findPaths] Finding paths with:", { - troopType, - armyStamina, - currentDefaultTick, - currentArmiesTick, - }); - const startPos = this._getCurrentPosition(); const maxHex = this._calculateMaxTravelPossible(currentDefaultTick, currentArmiesTick); const canExplore = this._canExplore(currentDefaultTick, currentArmiesTick); @@ -183,101 +175,78 @@ export class ArmyMovementManager { canExplore, }); - const priorityQueue: Array<{ position: HexPosition; staminaUsed: number; distance: number; path: HexTileInfo[] }> = - [ - { - position: startPos, - staminaUsed: 0, - distance: 0, - path: [{ col: startPos.col, row: startPos.row, biomeType: BiomeType.Grassland, staminaCost: 0 }], - }, - ]; + const startBiome = Biome.getBiome(startPos.col, startPos.row); + const travelPaths = new TravelPaths(); const lowestStaminaUse = new Map(); + const priorityQueue: Array<{ position: HexPosition; staminaUsed: number; distance: number; path: HexTileInfo[] }> = + []; + + // Process initial neighbors instead of start position + const neighbors = getNeighborHexes(startPos.col, startPos.row); + for (const { col, row } of neighbors) { + const isExplored = exploredHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + const hasArmy = armyHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + const hasStructure = structureHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; + const biome = exploredHexes.get(col - FELT_CENTER)?.get(row - FELT_CENTER); + + if (hasArmy || hasStructure) continue; + if (!isExplored && !canExplore) continue; + + const EXPLORATION_STAMINA_COST = configManager.getExploreStaminaCost(); + const staminaCost = biome ? ArmyMovementManager.staminaDrain(biome, troopType) : EXPLORATION_STAMINA_COST; + + if (staminaCost > armyStamina) continue; + + priorityQueue.push({ + position: { col, row }, + staminaUsed: staminaCost, + distance: 1, + path: [ + { col: startPos.col, row: startPos.row, biomeType: startBiome, staminaCost: 0 }, + { col, row, biomeType: biome, staminaCost }, + ], + }); + } while (priorityQueue.length > 0) { priorityQueue.sort((a, b) => a.staminaUsed - b.staminaUsed); const { position: current, staminaUsed, distance, path } = priorityQueue.shift()!; const currentKey = TravelPaths.posKey(current); - console.log("[findPaths] Processing position:", { - current, - staminaUsed, - distance, - pathLength: path.length, - }); - if (!lowestStaminaUse.has(currentKey) || staminaUsed < lowestStaminaUse.get(currentKey)!) { lowestStaminaUse.set(currentKey, staminaUsed); const isExplored = exploredHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - if (path.length >= 2) { - travelPaths.set(currentKey, { path, isExplored }); - } + travelPaths.set(currentKey, { path, isExplored }); - // Skip army and explored checks for start position - if (path.length > 1) { - if (!isExplored) continue; - - const hasArmy = armyHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - const hasStructure = structureHexes.get(current.col - FELT_CENTER)?.has(current.row - FELT_CENTER) || false; - console.log("[findPaths] Checking position:", { - col: current.col, - row: current.row, - isExplored, - hasArmy, - hasStructure, - }); - - if (hasArmy || hasStructure) continue; - } + if (!isExplored) continue; const neighbors = getNeighborHexes(current.col, current.row); for (const { col, row } of neighbors) { const neighborKey = TravelPaths.posKey({ col, row }); const nextDistance = distance + 1; + if (nextDistance > maxHex) continue; + const isExplored = exploredHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; const hasArmy = armyHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; const hasStructure = structureHexes.get(col - FELT_CENTER)?.has(row - FELT_CENTER) || false; const biome = exploredHexes.get(col - FELT_CENTER)?.get(row - FELT_CENTER); - const EXPLORATION_STAMINA_COST = configManager.getExploreStaminaCost(); - const staminaCost = biome ? ArmyMovementManager.staminaDrain(biome, troopType) : EXPLORATION_STAMINA_COST; + if (!isExplored || hasArmy || hasStructure) continue; + + const staminaCost = ArmyMovementManager.staminaDrain(biome!, troopType); const nextStaminaUsed = staminaUsed + staminaCost; - const nextPath = [...path, { col, row, biomeType: biome, staminaCost: staminaCost }]; - - console.log("[findPaths] Evaluating neighbor:", { - col, - row, - hasStructure, - hasArmy, - isExplored, - biome, - staminaCost, - nextStaminaUsed, - }); - - // if (!biome) continue; + if (nextStaminaUsed > armyStamina) continue; - if (hasStructure) continue; - if (hasArmy) continue; - - if ((isExplored && nextDistance <= maxHex) || (!isExplored && canExplore && nextDistance === 1)) { - if (!lowestStaminaUse.has(neighborKey) || nextStaminaUsed < lowestStaminaUse.get(neighborKey)!) { - console.log("[findPaths] Adding to priority queue:", { - col, - row, - nextDistance, - nextStaminaUsed, - }); - - priorityQueue.push({ - position: { col, row }, - staminaUsed: nextStaminaUsed, - distance: nextDistance, - path: nextPath, - }); - } + + if (!lowestStaminaUse.has(neighborKey) || nextStaminaUsed < lowestStaminaUse.get(neighborKey)!) { + priorityQueue.push({ + position: { col, row }, + staminaUsed: nextStaminaUsed, + distance: nextDistance, + path: [...path, { col, row, biomeType: biome, staminaCost }], + }); } } } From 7d4a1d091f7f2a3d6df64610128c70f667ded432 Mon Sep 17 00:00:00 2001 From: aymericdelab Date: Thu, 13 Feb 2025 10:35:46 +0100 Subject: [PATCH 09/10] fix max hex cost --- .../core/src/managers/army-movement-manager.ts | 15 ++++----------- packages/core/src/managers/config-manager.ts | 5 +++++ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/core/src/managers/army-movement-manager.ts b/packages/core/src/managers/army-movement-manager.ts index 6c3f6b1b8..8b0c2b3b4 100644 --- a/packages/core/src/managers/army-movement-manager.ts +++ b/packages/core/src/managers/army-movement-manager.ts @@ -83,11 +83,9 @@ export class ArmyMovementManager { private readonly _calculateMaxTravelPossible = (currentDefaultTick: number, currentArmiesTick: number) => { const stamina = this.staminaManager.getStamina(currentArmiesTick); - const travelStaminaCost = configManager.getTravelStaminaCost(); - - const maxStaminaSteps = travelStaminaCost - ? Math.floor((stamina.amount || 0) / configManager.getTravelStaminaCost()) - : 999; + // Calculate minimum stamina cost across all biomes for this troop type + const minTravelStaminaCost = configManager.getMinTravelStaminaCost(); + const maxStaminaSteps = Math.floor(stamina.amount / minTravelStaminaCost); const entityArmy = getComponentValue(this.components.Army, this.entity); const travelFoodCosts = computeTravelFoodCosts(entityArmy?.troops); @@ -166,15 +164,10 @@ export class ArmyMovementManager { ): TravelPaths { const troopType = this._getTroopType(); const startPos = this._getCurrentPosition(); + // max hex based on food const maxHex = this._calculateMaxTravelPossible(currentDefaultTick, currentArmiesTick); const canExplore = this._canExplore(currentDefaultTick, currentArmiesTick); - console.log("[findPaths] Initial conditions:", { - startPos, - maxHex, - canExplore, - }); - const startBiome = Biome.getBiome(startPos.col, startPos.row); const travelPaths = new TravelPaths(); diff --git a/packages/core/src/managers/config-manager.ts b/packages/core/src/managers/config-manager.ts index 0408b591a..aee621f70 100644 --- a/packages/core/src/managers/config-manager.ts +++ b/packages/core/src/managers/config-manager.ts @@ -296,6 +296,11 @@ export class ClientConfigManager { }, 0); } + // todo: need to get this from config + getMinTravelStaminaCost() { + return 10; + } + getResourceBridgeFeeSplitConfig() { return this.getValueOrDefault( () => { From 7944c0494cb5659f739441fca13616950afe9b44 Mon Sep 17 00:00:00 2001 From: aymericdelab Date: Thu, 13 Feb 2025 11:09:08 +0100 Subject: [PATCH 10/10] feedback --- .../game/src/three/managers/army-manager.ts | 18 ++++++++++++--- .../apps/game/src/three/scenes/worldmap.tsx | 22 +++++++++++++------ .../game/src/three/systems/system-manager.ts | 2 -- .../src/ui/elements/stamina-resource-cost.tsx | 4 ++-- .../src/managers/army-movement-manager.ts | 5 ++++- 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/client/apps/game/src/three/managers/army-manager.ts b/client/apps/game/src/three/managers/army-manager.ts index fa023f4d8..3d5d3ad71 100644 --- a/client/apps/game/src/three/managers/army-manager.ts +++ b/client/apps/game/src/three/managers/army-manager.ts @@ -4,7 +4,16 @@ import { isAddressEqualToAccount } from "@/three/helpers/utils"; import { ArmyModel } from "@/three/managers/army-model"; import { LabelManager } from "@/three/managers/label-manager"; import { Position } from "@/types/position"; -import { Biome, BiomeType, ContractAddress, FELT_CENTER, ID, orders } from "@bibliothecadao/eternum"; +import { + Biome, + BiomeType, + configManager, + ContractAddress, + FELT_CENTER, + ID, + orders, + ResourcesIds, +} from "@bibliothecadao/eternum"; import * as THREE from "three"; import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer"; import { findShortestPath } from "../helpers/pathfinding"; @@ -314,8 +323,11 @@ export class ArmyManager { if (startPos.x === targetPos.x && startPos.y === targetPos.y) return; - // todo: need to check better max distance - const path = findShortestPath(armyData.hexCoords, hexCoords, exploredTiles, structureHexes, armyHexes, 20); + // todo: currently taking max stamina of paladin as max stamina but need to refactor + const maxTroopStamina = configManager.getTroopStaminaConfig(ResourcesIds.Paladin); + const maxHex = Math.floor(maxTroopStamina / configManager.getMinTravelStaminaCost()); + + const path = findShortestPath(armyData.hexCoords, hexCoords, exploredTiles, structureHexes, armyHexes, maxHex); if (!path || path.length === 0) return; diff --git a/client/apps/game/src/three/scenes/worldmap.tsx b/client/apps/game/src/three/scenes/worldmap.tsx index 1991993b3..ffd1ba771 100644 --- a/client/apps/game/src/three/scenes/worldmap.tsx +++ b/client/apps/game/src/three/scenes/worldmap.tsx @@ -25,7 +25,6 @@ import { HexPosition, ID, SetupResult, - StaminaManager, TileManager, TravelPaths, getNeighborOffsets, @@ -55,9 +54,11 @@ export default class WorldmapScene extends HexagonScene { private structureManager: StructureManager; private battleManager: BattleManager; private exploredTiles: Map> = new Map(); + // normalized positions private armyHexes: Map> = new Map(); + // normalized positions private structureHexes: Map> = new Map(); - // normalized hex positions + // store armies positions by ID, to remove previous positions when army moves private armiesPositions: Map = new Map(); private battles: Map> = new Map(); private tileManager: TileManager; @@ -377,13 +378,10 @@ export default class WorldmapScene extends HexagonScene { const { currentDefaultTick, currentArmiesTick } = getBlockTimestamp(); - const stamina = new StaminaManager(this.dojo.components, selectedEntityId).getStamina(currentArmiesTick).amount; - const travelPaths = armyMovementManager.findPaths( this.structureHexes, this.armyHexes, this.exploredTiles, - stamina, currentDefaultTick, currentArmiesTick, ); @@ -423,6 +421,7 @@ export default class WorldmapScene extends HexagonScene { this.armyManager.removeLabelsFromScene(); } + // used to track the position of the armies on the map public updateArmyHexes(update: ArmySystemUpdate) { const { hexCoords: { col, row }, @@ -438,7 +437,7 @@ export default class WorldmapScene extends HexagonScene { // update the position of the army this.armiesPositions.set(update.entityId, { col: newCol, row: newRow }); - if (oldCol && oldRow) { + if (oldCol !== undefined && oldRow !== undefined) { if (oldCol !== newCol || oldRow !== newRow) { this.armyHexes.get(oldCol)?.delete(oldRow); if (!this.armyHexes.has(newCol)) { @@ -582,7 +581,16 @@ export default class WorldmapScene extends HexagonScene { return chunks; } - removeCachedMatricesAroundColRow(col: number, row: number) {} + removeCachedMatricesAroundColRow(col: number, row: number) { + for (let i = -this.renderChunkSize.width / 2; i <= this.renderChunkSize.width / 2; i += 10) { + for (let j = -this.renderChunkSize.width / 2; j <= this.renderChunkSize.height / 2; j += 10) { + if (i === 0 && j === 0) { + continue; + } + this.removeCachedMatricesForChunk(row + i, col + j); + } + } + } clearCache() { this.cachedMatrices.clear(); diff --git a/client/apps/game/src/three/systems/system-manager.ts b/client/apps/game/src/three/systems/system-manager.ts index 6f610b4d8..bb9095f0c 100644 --- a/client/apps/game/src/three/systems/system-manager.ts +++ b/client/apps/game/src/three/systems/system-manager.ts @@ -281,8 +281,6 @@ export class SystemManager { const newState = update.value[0]; const prevState = update.value[1]; - console.log("newState", newState); - const { col, row } = prevState || newState; return { hexCoords: { col, row }, diff --git a/client/apps/game/src/ui/elements/stamina-resource-cost.tsx b/client/apps/game/src/ui/elements/stamina-resource-cost.tsx index 68bc3d54e..05319e4a0 100644 --- a/client/apps/game/src/ui/elements/stamina-resource-cost.tsx +++ b/client/apps/game/src/ui/elements/stamina-resource-cost.tsx @@ -47,8 +47,8 @@ export const StaminaResourceCost = ({
{pathInfo.isExplored ? ( - path.map((tile, index) => ( -
+ path.map((tile) => ( +
{tile.biomeType}: {tile.staminaCost}
)) diff --git a/packages/core/src/managers/army-movement-manager.ts b/packages/core/src/managers/army-movement-manager.ts index 8b0c2b3b4..3e4e0547b 100644 --- a/packages/core/src/managers/army-movement-manager.ts +++ b/packages/core/src/managers/army-movement-manager.ts @@ -112,6 +112,7 @@ export class ArmyMovementManager { }; } + // todo : refactor to use stamina config public static staminaDrain(biome: BiomeType, troopType: TroopType) { const baseStaminaCost = 20; // Base cost to move to adjacent hex @@ -158,10 +159,12 @@ export class ArmyMovementManager { structureHexes: Map>, armyHexes: Map>, exploredHexes: Map>, - armyStamina: number, currentDefaultTick: number, currentArmiesTick: number, ): TravelPaths { + const armyStamina = this.staminaManager.getStamina(currentArmiesTick).amount; + if (armyStamina === 0) return new TravelPaths(); + const troopType = this._getTroopType(); const startPos = this._getCurrentPosition(); // max hex based on food