([]); + const [loading, setLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState (); + const [successMessage, setSuccessMessage] = useState (); + const { address, account } = useAccount(); + //const { balances, l2loading } = useWalletsProviderContext(); + const [selectedToken, setSelectedToken] = useState (SupportedToken.ETH); + + const selectedTokenObj = useMemo(() => { + return supportedTokens[chain][selectedToken]; + }, [selectedToken]); + + const { data, isLoading } = useBalance({ + address, + token: selectedTokenObj?.address as `0x${string}` | undefined, + watch: false, + }); + const isDebouncing = useDebounce(sellAmount, 350) !== sellAmount; + const isBuyInputDebouncing = useDebounce(buyAmount, 350) !== buyAmount; + + useEffect(() => { + if (initialLordsSupply) setBuyAmount(initialLordsSupply); + }, [initialLordsSupply]); + + const fetchSellAmountFromBuyAmount = useCallback(() => { + if (!selectedTokenObj || !buyAmount || isBuyInputDebouncing) return; + setLoading(true); + const params = { + sellTokenAddress: selectedTokenObj.address, + buyTokenAddress: lordsAddress, + sellAmount: parseUnits("1", 18), + takerAddress: address, + size: 1, + }; + + fetchQuotes(params, AVNU_OPTIONS) + .then((quotes) => { + setLoading(false); + if (quotes[0]) { + // cross-multiplication + // For 1 unit of tokenA => you get y amount of tokenB + // Then for x, a specific amount of tokenB => You need to have 1 * x / y + const sellAmountFromBuyAmount = (parseUnits("1", 18) * parseUnits(buyAmount, 18)) / quotes[0]?.buyAmount; + + setSellAmount(formatEther(sellAmountFromBuyAmount)); + } + }) + .catch(() => setLoading(false)); + }, [address, isBuyInputDebouncing, selectedTokenObj, buyAmount]); + + const fetchAvnuQuotes = useCallback(() => { + console.log(sellAmount); + if (!selectedTokenObj || !sellAmount || isDebouncing || parseUnits(sellAmount, selectedTokenObj.decimals) === 0n) + return; + setLoading(true); + const params = { + sellTokenAddress: selectedTokenObj.address ?? "0x", + buyTokenAddress: lordsAddress, + sellAmount: parseUnits(sellAmount, selectedTokenObj.decimals), + takerAddress: address, + size: 1, + }; + console.log(params); + fetchQuotes(params, AVNU_OPTIONS) + .then((quotes) => { + setLoading(false); + setQuotes(quotes); + }) + .catch(() => setLoading(false)); + }, [address, isDebouncing, selectedTokenObj, sellAmount]); + + const sellBalance = data?.value ?? 0; + + const handleChangeBuyInput = (event: ChangeEvent ) => { + setErrorMessage(""); + setQuotes([]); + setBuyAmount(event.target.value); + }; + + const handleTokenSelect = (event: string) => { + setLoading(true); + setQuotes([]); + setSelectedToken(event); + }; + + useEffect(() => { + if (sellAmount && selectedTokenObj && !isDebouncing) { + fetchAvnuQuotes(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedTokenObj, isDebouncing, sellAmount]); + + useEffect(() => { + if (buyAmount && selectedTokenObj && !isBuyInputDebouncing) { + fetchSellAmountFromBuyAmount(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedTokenObj, isBuyInputDebouncing, buyAmount]); + + const handleSwap = () => { + if (!account || !sellAmount || !quotes[0]) return; + setErrorMessage(""); + setSuccessMessage(""); + setLoading(true); + executeSwap(account, quotes[0], {}, AVNU_OPTIONS) + .then(() => { + setSuccessMessage("success"); + setLoading(false); + setQuotes([]); + }) + .catch((error: Error) => { + setLoading(false); + setErrorMessage(error.message); + }); + }; + + /*if (!account) { + return + }*/ + + const renderTokensInput = () => { + return ( + + + + ++ ); + }; + + const renderLordsInput = () => { + return ( ++ + ++ ); + }; + + const buttonContent = () => { + switch (true) { + case sellAmount === "0" || !sellAmount: + return "Enter amount"; + case loading: + return+++ Loading...
; + case parseEther(sellAmount ?? "0") > sellBalance: + return "Insufficient Balance"; + case !!quotes[0]: + return "Swap"; + + default: + return null; // Or any default case you'd like to handle + } + }; + + return ( + + ); +}; diff --git a/landing/src/components/modules/swap-panel.tsx b/landing/src/components/modules/swap-panel.tsx index 0f3f13740..0076deac3 100644 --- a/landing/src/components/modules/swap-panel.tsx +++ b/landing/src/components/modules/swap-panel.tsx @@ -1,7 +1,11 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useState } from "react"; +import { Button } from "../ui/button"; +import { LordsPurchaseDialog } from "./lords-purchase-dialog"; import { Swap } from "./swap"; export const SwapPanel = () => { + const [isLordsPurchaseDialogOpen, setIsLordsPurchaseDialogOpen] = useState(false); return (); }; diff --git a/landing/src/components/modules/token-balance.tsx b/landing/src/components/modules/token-balance.tsx new file mode 100644 index 000000000..f93a4c7fa --- /dev/null +++ b/landing/src/components/modules/token-balance.tsx @@ -0,0 +1,51 @@ +import { currencyFormat } from "@/lib/utils"; +import { Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; + + +export const TokenBalance = ({ + balance, + symbol, + isLoading, + onClick, +}: { + balance: bigint | number | string; + symbol: string; + isLoading?: boolean; + onClick?: () => void; +}) => { + const [balanceState, setBalanceState] = useState@@ -20,6 +24,9 @@ export const SwapPanel = () => { + ++ (); + useEffect(() => { + setBalanceState(balance); + }, [balance]); + return ( + ++ ); +}; \ No newline at end of file diff --git a/landing/src/components/providers/cartridge-controller.tsx b/landing/src/components/providers/cartridge-controller.tsx deleted file mode 100644 index 16ca3a125..000000000 --- a/landing/src/components/providers/cartridge-controller.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import ControllerConnector from "@cartridge/connector/controller"; - -export const cartridgeController = new ControllerConnector({ - rpc: "https://api.cartridge.gg/x/starknet/sepolia", - // Uncomment to use a custom theme - // theme: "dope-wars", - // colorMode: "light" -}); diff --git a/landing/src/config.ts b/landing/src/config.ts index 45275ad70..dd7139894 100644 --- a/landing/src/config.ts +++ b/landing/src/config.ts @@ -26,7 +26,7 @@ export const tokens: { } = { [Chain.MAINNET]: { [Token.LORDS]: { - address: "", + address: "0x0124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", decimals: 18, }, }, @@ -57,8 +57,71 @@ export const tokens: { }, }; -const chain = import.meta.env.VITE_PUBLIC_CHAIN as Chain | Chain.LOCAL; +export const chain = import.meta.env.VITE_PUBLIC_CHAIN as Chain | Chain.LOCAL; export const seasonPassAddress = tokens[chain][Token.SEASON_PASS]?.address as `0x${string}`; export const lordsAddress = tokens[chain][Token.LORDS]?.address as `0x${string}`; export const realmsAddress = tokens[chain][Token.REALMS]?.address as `0x${string}`; + +export enum SupportedToken { + ETH = "ETH", + WETH = "WETH", + USDC = "USDC", + USDT = "USDT", + STRK = "STRK", +} + +export const supportedTokens: { + [key in Chain]: { + [key in SupportedToken]?: { + address: string; + decimals: number; + symbol: string; + name: string; + logoURI: string; + isNative?: boolean; + }; + }; +} = { + [Chain.MAINNET]: { + [SupportedToken.ETH]: { + address: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + name: "Ethereum", + symbol: "ETH", + decimals: 18, + logoURI: "https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/0d9233eef112388ef7e261cb88413894fd832679/assets/tokensets/coin-icons/eth.svg", + isNative: true + }, + [SupportedToken.USDC]: { + address: "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8", + name: "USD Coin", + symbol: "USDC", + decimals: 6, + logoURI: "https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/0d9233eef112388ef7e261cb88413894fd832679/assets/tokensets/coin-icons/usdc.svg" + }, + [SupportedToken.USDT]: { + address: "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8", + name: "Tether USD", + symbol: "USDT", + decimals: 6, + logoURI: "https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/0d9233eef112388ef7e261cb88413894fd832679/assets/tokensets/coin-icons/usdt.svg" + }, + [SupportedToken.STRK]: { + address: "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + name: "Starknet Token", + symbol: "STRK", + decimals: 18, + logoURI: "https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/0d9233eef112388ef7e261cb88413894fd832679/assets/tokensets/coin-icons/eth.svg" + } + }, + [Chain.SEPOLIA]: { + // Add sepolia tokens here if needed + }, + [Chain.LOCAL]: { + + } +}; + +// Helper exports if needed +export const getSupportedTokenAddress = (token: SupportedToken) => + supportedTokens[chain][token]?.address as `0x${string}`; diff --git a/landing/src/dojo/setupNetwork.ts b/landing/src/dojo/setupNetwork.ts index 73174453a..db9b37a92 100644 --- a/landing/src/dojo/setupNetwork.ts +++ b/landing/src/dojo/setupNetwork.ts @@ -1,6 +1,5 @@ import { EternumProvider } from "@bibliothecadao/eternum"; import { DojoConfig } from "@dojoengine/core"; -import { world } from "./world"; import { BurnerManager } from "@dojoengine/create-burner"; import * as torii from "@dojoengine/torii-client"; @@ -40,7 +39,6 @@ export async function setupNetwork({ ...config }: DojoConfig) { return { toriiClient, provider, - world, burnerManager, }; } diff --git a/landing/src/dojo/world.ts b/landing/src/dojo/world.ts deleted file mode 100644 index 960676e5e..000000000 --- a/landing/src/dojo/world.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createWorld } from "@dojoengine/recs"; - -export const world = createWorld(); diff --git a/landing/src/hooks/use-debounce.tsx b/landing/src/hooks/use-debounce.tsx new file mode 100644 index 000000000..9f88d4d1a --- /dev/null +++ b/landing/src/hooks/use-debounce.tsx @@ -0,0 +1,26 @@ +import { useEffect, useState } from 'react' + +/** + * Debounces updates to a value. + * Non-primitives *must* wrap the value in useMemo, or the value will be updated due to referential inequality. + */ +// modified from https://usehooks.com/useDebounce/ +export default function useDebounce+++ Balance: + + {isLoading ?? typeof balanceState != "bigint" ? ( ++ {/*+ ) : balanceState ? ( + currencyFormat(Number(balanceState), 3) + .toLocaleString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ) : ( + "0" + )} + {symbol}+*/} + (value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState (value) + + useEffect(() => { + // Update debounced value after delay + const handler = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + // Cancel the timeout if value changes (also on delay change or unmount) + // This is how we prevent debounced value from updating if value is changed ... + // .. within the delay period. Timeout gets cleared and restarted. + return () => { + clearTimeout(handler) + } + }, [value, delay]) + + return debouncedValue +} \ No newline at end of file diff --git a/landing/src/main.tsx b/landing/src/main.tsx index 3a11be958..665721e60 100644 --- a/landing/src/main.tsx +++ b/landing/src/main.tsx @@ -13,6 +13,7 @@ import { StarknetProvider } from "./components/providers/Starknet"; import { ThemeProvider } from "./components/providers/theme-provider"; import { DojoProvider } from "./hooks/context/DojoContext"; import { routeTree } from "./routeTree.gen"; +import Renderer from "./three/Renderer"; // Create a new router instance const router = createRouter({ routeTree }); @@ -25,6 +26,9 @@ declare module "@tanstack/react-router" { // Render the app const rootElement = document.getElementById("root")!; + +const graphic = new Renderer(); + if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); const setupResult = await setup(dojoConfig); diff --git a/landing/src/three/LandingHexagonScene.ts b/landing/src/three/LandingHexagonScene.ts new file mode 100644 index 000000000..eadfb45d7 --- /dev/null +++ b/landing/src/three/LandingHexagonScene.ts @@ -0,0 +1,156 @@ +import * as THREE from "three"; +import { MapControls } from "three/examples/jsm/controls/MapControls.js"; +import { BiomeType } from "./components/Biome"; +import InstancedBiome from "./components/InstancedBiome"; +import { biomeModelPaths } from "./constants"; +import { getWorldPositionForHex, gltfLoader } from "./helpers/utils"; + +export class HexagonScene { + protected scene!: THREE.Scene; + protected camera!: THREE.PerspectiveCamera; + protected biomeModels: Map = new Map(); + protected modelLoadPromises: Promise [] = []; + protected fog!: THREE.Fog; + + private mainDirectionalLight!: THREE.DirectionalLight; + private hemisphereLight!: THREE.HemisphereLight; + private groundMesh!: THREE.Mesh; + + constructor(protected controls: MapControls) { + this.controls = controls; + this.initializeScene(); + this.setupLighting(); + this.createGroundMesh(); + } + + private initializeScene(): void { + this.scene = new THREE.Scene(); + this.camera = this.controls.object as THREE.PerspectiveCamera; + this.scene.background = new THREE.Color(0x8790a1); + this.fog = new THREE.Fog(0xffffff, 21, 30); + this.scene.fog = this.fog; + } + + private setupLighting(): void { + this.hemisphereLight = new THREE.HemisphereLight(0xf3f3c8, 0xd0e7f0, 0.3); + + this.mainDirectionalLight = new THREE.DirectionalLight(0xffffff, 1.4); + this.mainDirectionalLight.castShadow = true; + this.mainDirectionalLight.shadow.mapSize.width = 1024; + this.mainDirectionalLight.shadow.mapSize.height = 1024; + this.mainDirectionalLight.shadow.camera.left = -22; + this.mainDirectionalLight.shadow.camera.right = 18; + this.mainDirectionalLight.shadow.camera.top = 14; + this.mainDirectionalLight.shadow.camera.bottom = -12; + this.mainDirectionalLight.shadow.camera.far = 38; + this.mainDirectionalLight.shadow.camera.near = 8; + this.mainDirectionalLight.shadow.bias = -0.0015; + this.mainDirectionalLight.position.set(0, 9, 0); + this.mainDirectionalLight.target.position.set(0, 0, 5.2); + + this.scene.add(this.mainDirectionalLight); + this.scene.add(this.mainDirectionalLight.target); + } + + private createGroundMesh() { + const scale = 60; + const metalness = 0; + const roughness = 0.1; + + const geometry = new THREE.PlaneGeometry(2668, 1390.35); + const texture = new THREE.TextureLoader().load("/textures/paper/worldmap-bg.png", () => { + texture.colorSpace = THREE.SRGBColorSpace; + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(scale, scale / 2.5); + }); + + const material = new THREE.MeshStandardMaterial({ + map: texture, + metalness: metalness, + roughness: roughness, + side: THREE.DoubleSide, + }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.rotation.set(Math.PI / 2, 0, Math.PI); + mesh.position.set(0, -0.05, 0); + mesh.receiveShadow = true; + + this.scene.add(mesh); + this.groundMesh = mesh; + } + + public getScene() { + return this.scene; + } + + public getCamera() { + return this.camera; + } + + public setEnvironment(texture: THREE.Texture, intensity: number = 1) { + this.scene.environment = texture; + this.scene.environmentIntensity = intensity; + } + + loadBiomeModels(maxInstances: number) { + const loader = gltfLoader; + + for (const [biome, path] of Object.entries(biomeModelPaths)) { + const loadPromise = new Promise ((resolve, reject) => { + loader.load( + path, + (gltf) => { + const model = gltf.scene as THREE.Group; + if (biome === "Outline") { + ((model.children[0] as THREE.Mesh).material as THREE.MeshStandardMaterial).transparent = true; + ((model.children[0] as THREE.Mesh).material as THREE.MeshStandardMaterial).opacity = 0.3; + } + const tmp = new InstancedBiome(gltf, maxInstances, false, biome); + this.biomeModels.set(biome as BiomeType, tmp); + this.scene.add(tmp.group); + resolve(); + }, + undefined, + (error) => { + console.error(`Error loading ${biome} model:`, error); + reject(error); + }, + ); + }); + this.modelLoadPromises.push(loadPromise); + } + } + + public moveCameraToColRow(col: number, row: number, duration: number = 2) { + const { x, y, z } = getWorldPositionForHex({ col, row }); + + const newTarget = new THREE.Vector3(x, y, z); + + const target = this.controls.target; + const pos = this.controls.object.position; + + const deltaX = newTarget.x - target.x; + const deltaZ = newTarget.z - target.z; + target.set(newTarget.x, newTarget.y, newTarget.z); + pos.set(pos.x + deltaX, pos.y, pos.z + deltaZ); + this.controls.update(); + } + + update(deltaTime: number): void { + this.updateLights(); + this.biomeModels.forEach((biome) => { + biome.updateAnimations(deltaTime); + }); + } + + private updateLights(): void { + if (this.mainDirectionalLight) { + const { x, y, z } = this.controls.target; + this.mainDirectionalLight.position.set(x - 15, y + 13, z + 8); + this.mainDirectionalLight.target.position.set(x, y, z - 5.2); + this.mainDirectionalLight.target.updateMatrixWorld(); + } + } +} diff --git a/landing/src/three/LandingHexceptionScene.ts b/landing/src/three/LandingHexceptionScene.ts new file mode 100644 index 000000000..5fbc36353 --- /dev/null +++ b/landing/src/three/LandingHexceptionScene.ts @@ -0,0 +1,450 @@ +import { HexPosition, ResourceMiningTypes } from "@/types"; +import { BuildingType, getNeighborHexes, RealmLevels, ResourcesIds } from "@bibliothecadao/eternum"; +import * as THREE from "three"; +import { MapControls } from "three/examples/jsm/controls/MapControls.js"; +import { Biome, BIOME_COLORS, BiomeType } from "./components/Biome"; +import { + buildingModelPaths, + BUILDINGS_CENTER, + castleLevelToRealmCastle, + HEX_SIZE, + MinesMaterialsParams, +} from "./constants"; +import { createHexagonShape } from "./geometry/HexagonGeometry"; +import { getWorldPositionForHex, gltfLoader, ResourceIdToMiningType } from "./helpers/utils"; +import { HexagonScene } from "./LandingHexagonScene"; + +const loader = gltfLoader; + +const generateHexPositions = (center: HexPosition, radius: number) => { + const color = new THREE.Color("gray"); + const positions: any[] = []; + const positionSet = new Set(); + + const addPosition = (col: number, row: number, isBorder: boolean) => { + const key = `${col},${row}`; + if (!positionSet.has(key)) { + const position = { + ...getWorldPositionForHex({ col, row }, false), + color, + col, + row, + isBorder, + }; + positions.push(position); + positionSet.add(key); + } + }; + + addPosition(center.col, center.row, false); + + let currentLayer = [center]; + for (let i = 0; i < radius; i++) { + const nextLayer: any = []; + currentLayer.forEach((pos) => { + getNeighborHexes(pos.col, pos.row).forEach((neighbor) => { + if (!positionSet.has(`${neighbor.col},${neighbor.row}`)) { + addPosition(neighbor.col, neighbor.row, i === radius - 1); + nextLayer.push({ col: neighbor.col, row: neighbor.row }); + } + }); + }); + currentLayer = nextLayer; + } + + return positions; +}; + +export default class LandingHexceptionScene extends HexagonScene { + private hexceptionRadius = 4; + private buildingModels: Map = new Map(); + private buildingInstances: Map = new Map(); + private buildingMixers: Map = new Map(); + private pillars: THREE.InstancedMesh | null = null; + private buildings: any = []; + centerColRow: number[] = [0, 0]; + castleLevel: RealmLevels = RealmLevels.Settlement; + private biome!: Biome; + private minesMaterials: Map = new Map(); + + constructor(controls: MapControls) { + super(controls); + + this.biome = new Biome(); + + const pillarGeometry = new THREE.ExtrudeGeometry(createHexagonShape(1), { depth: 2, bevelEnabled: false }); + pillarGeometry.rotateX(Math.PI / 2); + this.pillars = new THREE.InstancedMesh(pillarGeometry, new THREE.MeshStandardMaterial(), 1000); + this.pillars.position.y = 0.05; + this.pillars.count = 0; + this.scene.add(this.pillars); + + this.loadBuildingModels(); + this.loadBiomeModels(900); + + this.setup(); + + this.controls.maxDistance = 18; + this.controls.enablePan = false; + this.controls.zoomToCursor = false; + } + + private loadBuildingModels() { + for (const [building, path] of Object.entries(buildingModelPaths)) { + const loadPromise = new Promise ((resolve, reject) => { + loader.load( + path, + (gltf) => { + const model = gltf.scene as THREE.Group; + model.position.set(0, 0, 0); + model.rotation.y = Math.PI; + + model.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + this.buildingModels.set(building, { model, animations: gltf.animations }); + resolve(); + }, + undefined, + (error) => { + console.error(`Error loading ${building} model:`, error); + reject(error); + }, + ); + }); + this.modelLoadPromises.push(loadPromise); + } + } + + setup() { + this.centerColRow = [Math.floor(Math.random() * 401) - 200, Math.floor(Math.random() * 401) - 200]; + + const randomCastleLevel = Math.floor(Math.random() * 4); + this.castleLevel = randomCastleLevel; + + this.updateHexceptionGrid(this.hexceptionRadius); + this.moveCameraToCenter(); + } + + private generateRandomBuildings() { + const buildings = []; + + const mainBuilding = Math.random() < 0.05 ? BuildingType.Bank : BuildingType.Castle; + buildings.push({ + col: BUILDINGS_CENTER[0], + row: BUILDINGS_CENTER[1], + category: BuildingType[mainBuilding], + resource: undefined, + paused: false, + }); + + const buildablePositions = generateHexPositions( + { col: BUILDINGS_CENTER[0], row: BUILDINGS_CENTER[1] }, + this.castleLevel + 1, + ).filter((pos) => !(pos.col === BUILDINGS_CENTER[0] && pos.row === BUILDINGS_CENTER[1])); + + const numBuildings = Math.floor(Math.random() * (buildablePositions.length - 3)) + 3; + + const availableBuildingTypes = [ + BuildingType.Resource, + BuildingType.Farm, + BuildingType.FishingVillage, + BuildingType.Barracks, + BuildingType.Market, + BuildingType.ArcheryRange, + BuildingType.Stable, + BuildingType.WorkersHut, + BuildingType.Storehouse, + ]; + + const availableResources = [ + ResourcesIds.Wood, + ResourcesIds.Stone, + ResourcesIds.Coal, + ResourcesIds.Copper, + ResourcesIds.Ironwood, + ResourcesIds.Obsidian, + ResourcesIds.Gold, + ResourcesIds.Silver, + ResourcesIds.Mithral, + ResourcesIds.AlchemicalSilver, + ResourcesIds.ColdIron, + ResourcesIds.DeepCrystal, + ResourcesIds.Ruby, + ResourcesIds.Diamonds, + ResourcesIds.Hartwood, + ResourcesIds.Ignium, + ResourcesIds.TwilightQuartz, + ResourcesIds.TrueIce, + ResourcesIds.Adamantine, + ResourcesIds.Sapphire, + ResourcesIds.EtherealSilica, + ]; + + const usedPositions = new Set(); + + for (let i = 0; i < Math.min(numBuildings, buildablePositions.length); i++) { + let randomPositionIndex; + let position; + + do { + randomPositionIndex = Math.floor(Math.random() * buildablePositions.length); + position = buildablePositions[randomPositionIndex]; + } while (usedPositions.has(`${position.col},${position.row}`)); + + usedPositions.add(`${position.col},${position.row}`); + + const randomBuildingType = availableBuildingTypes[Math.floor(Math.random() * availableBuildingTypes.length)]; + let randomResource = undefined; + if (randomBuildingType === BuildingType.Resource) { + randomResource = availableResources[Math.floor(Math.random() * availableResources.length)]; + } + if (randomBuildingType === BuildingType.Farm) { + randomResource = ResourcesIds.Wheat; + } + if (randomBuildingType === BuildingType.FishingVillage) { + randomResource = ResourcesIds.Fish; + } + + buildings.push({ + col: position.col, + row: position.row, + category: BuildingType[randomBuildingType], + resource: randomResource, + paused: false, + }); + } + + return buildings; + } + + public moveCameraToCenter() { + this.moveCameraToColRow(10, 10, 0); + } + + updateHexceptionGrid(radius: number) { + const dummy = new THREE.Object3D(); + + const biomeHexes: Record = { + Ocean: [], + DeepOcean: [], + Beach: [], + Scorched: [], + Bare: [], + Tundra: [], + Snow: [], + TemperateDesert: [], + Shrubland: [], + Taiga: [], + Grassland: [], + TemperateDeciduousForest: [], + TemperateRainForest: [], + SubtropicalDesert: [], + TropicalSeasonalForest: [], + TropicalRainForest: [], + }; + + Promise.all(this.modelLoadPromises).then(() => { + const centers = [ + [0, 0], // 0, 0 (Main hex) + [-6, 5], // -1, 1 + [7, 4], // 1, 0 + [1, 9], // 0, 1 + [-7, -4], // -1, 0 + [0, -9], // 0, -1 + [7, -5], // 1, -1 + ]; + + for (const center of centers) { + const isMainHex = center[0] === 0 && center[1] === 0; + const targetHex = { col: center[0] + this.centerColRow[0], row: center[1] + this.centerColRow[1] }; + this.computeHexMatrices( + radius, + dummy, + center, + targetHex, + isMainHex, + this.generateRandomBuildings(), + biomeHexes, + ); + } + + for (const building of this.buildings) { + const key = `${building.col},${building.row}`; + if (!this.buildingInstances.has(key)) { + let buildingType = + building.resource && (building.resource < 24 || building.resource === ResourcesIds.AncientFragment) + ? ResourceIdToMiningType[building.resource as ResourcesIds] + : (BuildingType[building.category].toString() as any); + + if (parseInt(buildingType) === BuildingType.Castle) { + buildingType = castleLevelToRealmCastle[this.castleLevel]; + } + const buildingData = this.buildingModels.get(buildingType); + + if (buildingData) { + const instance = buildingData.model.clone(); + instance.applyMatrix4(building.matrix); + if (buildingType === ResourceMiningTypes.Forge) { + instance.traverse((child) => { + if (child.name === "Grassland003_8" && child instanceof THREE.Mesh) { + if (!this.minesMaterials.has(building.resource)) { + const material = new THREE.MeshStandardMaterial(MinesMaterialsParams[building.resource]); + this.minesMaterials.set(building.resource, material); + } + child.material = this.minesMaterials.get(building.resource); + } + }); + } + if (buildingType === ResourceMiningTypes.Mine) { + const crystalMesh1 = instance.children[1] as THREE.Mesh; + const crystalMesh2 = instance.children[2] as THREE.Mesh; + if (!this.minesMaterials.has(building.resource)) { + const material = new THREE.MeshStandardMaterial(MinesMaterialsParams[building.resource]); + this.minesMaterials.set(building.resource, material); + } + // @ts-ignoreq + crystalMesh1.material = this.minesMaterials.get(building.resource); + // @ts-ignore + crystalMesh2.material = this.minesMaterials.get(building.resource); + } + this.scene.add(instance); + this.buildingInstances.set(key, instance); + + const animations = buildingData.animations; + if (animations && animations.length > 0) { + const mixer = new THREE.AnimationMixer(instance); + animations.forEach((clip: THREE.AnimationClip) => { + mixer.clipAction(clip).play(); + }); + this.buildingMixers.set(key, mixer); + } + } + } + } + + let pillarOffset = 0; + for (const [biome, matrices] of Object.entries(biomeHexes)) { + const hexMesh = this.biomeModels.get(biome as BiomeType)!; + matrices.forEach((matrix, index) => { + hexMesh.setMatrixAt(index, matrix); + this.pillars!.setMatrixAt(index + pillarOffset, matrix); + this.pillars!.setColorAt(index + pillarOffset, BIOME_COLORS[biome as BiomeType]); + }); + pillarOffset += matrices.length; + this.pillars!.count = pillarOffset; + this.pillars!.computeBoundingSphere(); + hexMesh.setCount(matrices.length); + } + + this.pillars!.instanceMatrix.needsUpdate = true; + this.pillars!.instanceColor!.needsUpdate = true; + }); + } + + computeHexMatrices = ( + radius: number, + dummy: THREE.Object3D, + center: number[], + targetHex: HexPosition, + isMainHex: boolean, + existingBuildings: any[], + biomeHexes: Record , + ) => { + const biome = this.biome.getBiome(targetHex.col, targetHex.row); + const buildableAreaBiome = "Grassland"; + const isFlat = biome === "Ocean" || biome === "DeepOcean" || isMainHex; + + let positions = generateHexPositions( + { col: center[0] + BUILDINGS_CENTER[0], row: center[1] + BUILDINGS_CENTER[1] }, + radius, + ); + + if (isMainHex) { + const buildablePositions = generateHexPositions( + { col: center[0] + BUILDINGS_CENTER[0], row: center[1] + BUILDINGS_CENTER[1] }, + this.castleLevel + 1, + ); + + positions = positions.filter( + (position) => + !buildablePositions.some( + (buildablePosition) => buildablePosition.col === position.col && buildablePosition.row === position.row, + ), + ); + + buildablePositions.forEach((position) => { + dummy.position.x = position.x; + dummy.position.z = position.z; + dummy.position.y = isMainHex || isFlat || position.isBorder ? 0 : position.y / 2; + dummy.scale.set(HEX_SIZE, HEX_SIZE, HEX_SIZE); + dummy.updateMatrix(); + biomeHexes[buildableAreaBiome].push(dummy.matrix.clone()); + const building = existingBuildings.find((value) => value.col === position.col && value.row === position.row); + if (building) { + const buildingObj = dummy.clone(); + const rotation = Math.PI / 3; + buildingObj.rotation.y = rotation * 4; + if (building.category === BuildingType[BuildingType.Castle]) { + buildingObj.rotation.y = rotation * 2; + } + if ( + BuildingType[building.category as keyof typeof BuildingType] === BuildingType.Resource && + ResourceIdToMiningType[building.resource as ResourcesIds] === ResourceMiningTypes.LumberMill + ) { + buildingObj.rotation.y = rotation * 2; + } + if ( + BuildingType[building.category as keyof typeof BuildingType] === BuildingType.Resource && + ResourceIdToMiningType[building.resource as ResourcesIds] === ResourceMiningTypes.Forge + ) { + buildingObj.rotation.y = rotation * 6; + } + if (building.resource && building.resource === ResourcesIds.Crossbowman) { + buildingObj.rotation.y = rotation; + } + if (building.resource && building.resource === ResourcesIds.Paladin) { + buildingObj.rotation.y = rotation * 3; + } + buildingObj.updateMatrix(); + this.buildings.push({ ...building, matrix: buildingObj.matrix.clone() }); + } + }); + } + + positions.forEach((position) => { + dummy.position.x = position.x; + dummy.position.z = position.z; + dummy.position.y = isMainHex || isFlat || position.isBorder ? 0 : position.y / 2; + dummy.scale.set(HEX_SIZE, HEX_SIZE, HEX_SIZE); + const rotationSeed = this.hashCoordinates(position.col, position.row); + const rotationIndex = Math.floor(rotationSeed * 6); + const randomRotation = (rotationIndex * Math.PI) / 3; + dummy.rotation.y = randomRotation; + dummy.updateMatrix(); + biomeHexes[biome].push(dummy.matrix.clone()); + }); + }; + + private hashCoordinates(x: number, y: number): number { + const str = `${x},${y}`; + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + return Math.abs(hash) / 2147483647; + } + + update(deltaTime: number) { + super.update(deltaTime); + this.buildingMixers.forEach((mixer) => { + mixer.update(deltaTime); + }); + } +} diff --git a/landing/src/three/Renderer.ts b/landing/src/three/Renderer.ts new file mode 100644 index 000000000..5eddee865 --- /dev/null +++ b/landing/src/three/Renderer.ts @@ -0,0 +1,147 @@ +import { + BloomEffect, + BrightnessContrastEffect, + EffectComposer, + EffectPass, + FXAAEffect, + RenderPass, +} from "postprocessing"; +import * as THREE from "three"; +import { MapControls } from "three/examples/jsm/controls/MapControls.js"; +import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; +import HexceptionScene from "./LandingHexceptionScene"; + +export default class Renderer { + private renderer!: THREE.WebGLRenderer; + private camera!: THREE.PerspectiveCamera; + private controls!: MapControls; + private composer!: EffectComposer; + private renderPass!: RenderPass; + private scene!: HexceptionScene; + + private cameraDistance = Math.sqrt(2 * 7 * 7); + private cameraAngle = 60 * (Math.PI / 180); + + private lastTime: number = 0; + + constructor() { + this.initializeRenderer(); + this.setupCamera(); + this.setupControls(); + this.initializeScene(); + this.setupPostProcessing(); + this.setupListeners(); + this.applyEnvironment(); + this.animate(); + } + + private initializeRenderer() { + this.renderer = new THREE.WebGLRenderer({ + powerPreference: "high-performance", + antialias: false, + stencil: false, + depth: false, + }); + this.renderer.setPixelRatio(0.75); + this.renderer.shadowMap.enabled = true; + this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; + this.renderer.setSize(window.innerWidth, window.innerHeight); + this.renderer.toneMapping = THREE.NoToneMapping; + this.renderer.toneMappingExposure = 1; + this.renderer.autoClear = false; + + this.composer = new EffectComposer(this.renderer, { + frameBufferType: THREE.HalfFloatType, + }); + } + + private initializeScene() { + this.scene = new HexceptionScene(this.controls); + document.body.prepend(this.renderer.domElement); + } + + private setupCamera() { + this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 30); + const cameraHeight = Math.sin(this.cameraAngle) * this.cameraDistance; + const cameraDepth = Math.cos(this.cameraAngle) * this.cameraDistance; + this.camera.position.set(0, cameraHeight, cameraDepth); + this.camera.lookAt(0, 0, 0); + this.camera.up.set(0, 1, 0); + } + + private setupControls() { + this.controls = new MapControls(this.camera, this.renderer.domElement); + this.controls.enableRotate = false; + this.controls.enableZoom = false; + this.controls.enablePan = false; + this.controls.panSpeed = 1; + this.controls.zoomToCursor = true; + this.controls.minDistance = 17; + this.controls.maxDistance = 17; + this.controls.enableDamping = true; + this.controls.dampingFactor = 0.1; + this.controls.target.set(0, 0, 0); + } + + private setupPostProcessing() { + this.renderPass = new RenderPass(this.scene.getScene(), this.camera); + this.composer.addPass(this.renderPass); + + const BCEffect = new BrightnessContrastEffect({ + brightness: -0.1, + contrast: 0, + }); + + this.composer.addPass( + new EffectPass( + this.camera, + new FXAAEffect(), + new BloomEffect({ + luminanceThreshold: 1.1, + mipmapBlur: true, + intensity: 0.25, + }), + BCEffect, + ), + ); + } + + private applyEnvironment() { + const pmremGenerator = new THREE.PMREMGenerator(this.renderer); + pmremGenerator.compileEquirectangularShader(); + + const hdriLoader = new RGBELoader(); + hdriLoader.load("/textures/environment/models_env.hdr", (texture) => { + const envMap = pmremGenerator.fromEquirectangular(texture).texture; + texture.dispose(); + this.scene.setEnvironment(envMap, 0.7); + }); + } + + private setupListeners() { + window.addEventListener("resize", this.onWindowResize.bind(this)); + } + + private onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + this.camera.aspect = width / height; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(width, height); + this.composer.setSize(width, height); + } + + animate() { + const currentTime = performance.now(); + const deltaTime = (currentTime - this.lastTime) / 1000; + this.lastTime = currentTime; + + this.controls.update(); + this.scene.update(deltaTime); + this.renderer.clear(); + this.composer.render(); + + requestAnimationFrame(() => this.animate()); + } +} diff --git a/landing/src/three/components/Biome.ts b/landing/src/three/components/Biome.ts new file mode 100644 index 000000000..701265245 --- /dev/null +++ b/landing/src/three/components/Biome.ts @@ -0,0 +1,115 @@ +import { snoise } from "@dojoengine/utils"; +import * as THREE from "three"; + +const MAP_AMPLITUDE = 60; +const MOISTURE_OCTAVE = 2; +const ELEVATION_OCTAVES = [1, 0.25, 0.1]; +const ELEVATION_OCTAVES_SUM = ELEVATION_OCTAVES.reduce((a, b) => a + b, 0); + +export type BiomeType = + | "DeepOcean" + | "Ocean" + | "Beach" + | "Scorched" + | "Bare" + | "Tundra" + | "Snow" + | "TemperateDesert" + | "Shrubland" + | "Taiga" + | "Grassland" + | "TemperateDeciduousForest" + | "TemperateRainForest" + | "SubtropicalDesert" + | "TropicalSeasonalForest" + | "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: 0.25, + OCEAN: 0.5, + SAND: 0.53, + FOREST: 0.6, + DESERT: 0.72, + MOUNTAIN: 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: number, + octaves: number[], + octavesSum: number, + ): number { + let elevation = 0; + for (const octave of octaves) { + const x = col / octave / mapAmplitude; + const z = row / octave / mapAmplitude; + const noise = ((snoise([x, 0, z]) + 1) * 100) / 2; + elevation += octave * Math.floor(noise); + } + elevation = elevation / octavesSum; + return elevation / 100; + } + + private calculateMoisture(col: number, row: number, mapAmplitude: number, moistureOctave: number): number { + const moistureX = (moistureOctave * col) / mapAmplitude; + const moistureZ = (moistureOctave * row) / mapAmplitude; + const noise = ((snoise([moistureX, 0, moistureZ]) + 1) * 100) / 2; + return Math.floor(noise) / 100; + } + + private determineBiome(elevation: number, moisture: number, level: typeof LEVEL): BiomeType { + if (elevation < level.DEEP_OCEAN) return "DeepOcean"; + if (elevation < level.OCEAN) return "Ocean"; + if (elevation < level.SAND) return "Beach"; + if (elevation > level.MOUNTAIN) { + if (moisture < 0.1) return "Scorched"; + if (moisture < 0.4) return "Bare"; + if (moisture < 0.5) return "Tundra"; + return "Snow"; + } + if (elevation > level.DESERT) { + if (moisture < 0.33) return "TemperateDesert"; + if (moisture < 0.66) return "Shrubland"; + return "Taiga"; + } + if (elevation > level.FOREST) { + if (moisture < 0.16) return "TemperateDesert"; + if (moisture < 0.5) return "Grassland"; + if (moisture < 0.83) return "TemperateDeciduousForest"; + return "TemperateRainForest"; + } + if (moisture < 0.16) return "SubtropicalDesert"; + if (moisture < 0.33) return "Grassland"; + if (moisture < 0.66) return "TropicalSeasonalForest"; + return "TropicalRainForest"; + } +} diff --git a/landing/src/three/components/InstancedBiome.ts b/landing/src/three/components/InstancedBiome.ts new file mode 100644 index 000000000..99d2786e4 --- /dev/null +++ b/landing/src/three/components/InstancedBiome.ts @@ -0,0 +1,156 @@ +import * as THREE from "three"; +import { AnimationClip, AnimationMixer } from "three"; +import { LAND_NAME, PREVIEW_BUILD_COLOR_INVALID } from "../constants"; + +const zeroScaledMatrix = new THREE.Matrix4().makeScale(0, 0, 0); +export default class InstancedModel { + public group: THREE.Group; + public instancedMeshes: THREE.InstancedMesh[] = []; + private biomeMeshes: any[] = []; + private count: number = 0; + private mixer: AnimationMixer | null = null; + private animation: AnimationClip | null = null; + private animationActions: Map = new Map(); + timeOffsets: Float32Array; + + constructor(gltf: any, count: number, enableRaycast: boolean = false, name: string = "") { + this.group = new THREE.Group(); + this.count = count; + + this.timeOffsets = new Float32Array(count); + for (let i = 0; i < count; i++) { + this.timeOffsets[i] = Math.random() * 3; + } + gltf.scene.traverse((child: any) => { + if (child instanceof THREE.Mesh) { + const tmp = new THREE.InstancedMesh(child.geometry, child.material, count); + const biomeMesh = child; + if (gltf.animations.length > 0) { + for (let i = 0; i < count; i++) { + tmp.setMorphAt(i, biomeMesh as any); + } + tmp.morphTexture!.needsUpdate = true; + } + + if (name !== "Outline" && !name.toLowerCase().includes("ocean")) { + tmp.castShadow = true; + tmp.receiveShadow = true; + } + tmp.userData.isInstanceModel = true; + + if (!enableRaycast) { + tmp.raycast = () => {}; + } + + this.mixer = new AnimationMixer(gltf.scene); + this.animation = gltf.animations[0]; + + tmp.count = 0; + this.group.add(tmp); + this.instancedMeshes.push(tmp); + this.biomeMeshes.push(biomeMesh); + } + }); + } + + getCount(): number { + return this.count; + } + + getLandColor() { + const land = this.group.children.find((child) => child.name === LAND_NAME); + if (land instanceof THREE.InstancedMesh) { + return (land.material as THREE.MeshStandardMaterial).color; + } + return new THREE.Color(PREVIEW_BUILD_COLOR_INVALID); + } + + getMatricesAndCount() { + return { + matrices: (this.group.children[0] as THREE.InstancedMesh).instanceMatrix.clone(), + count: (this.group.children[0] as THREE.InstancedMesh).count, + }; + } + + setMatricesAndCount(matrices: THREE.InstancedBufferAttribute, count: number) { + this.group.children.forEach((child) => { + if (child instanceof THREE.InstancedMesh) { + child.instanceMatrix.copy(matrices); + child.count = count; + child.instanceMatrix.needsUpdate = true; + } + }); + } + + setMatrixAt(index: number, matrix: THREE.Matrix4) { + this.group.children.forEach((child) => { + if (child instanceof THREE.InstancedMesh) { + child.setMatrixAt(index, matrix); + } + }); + } + + setColorAt(index: number, color: THREE.Color) { + this.group.children.forEach((child) => { + if (child instanceof THREE.InstancedMesh) { + child.setColorAt(index, color); + } + }); + } + + setCount(count: number) { + this.count = count; + this.group.children.forEach((child) => { + if (child instanceof THREE.InstancedMesh) { + child.count = count; + } + }); + this.needsUpdate(); + } + + removeInstance(index: number) { + this.setMatrixAt(index, zeroScaledMatrix); + this.needsUpdate(); + } + + needsUpdate() { + this.group.children.forEach((child) => { + if (child instanceof THREE.InstancedMesh) { + child.instanceMatrix.needsUpdate = true; + child.computeBoundingSphere(); + child.frustumCulled = false; + } + }); + } + + clone() { + return this.group.clone(); + } + + scaleModel(scale: THREE.Vector3) { + this.group.scale.copy(scale); + this.group.updateMatrixWorld(true); + } + + updateAnimations(deltaTime: number) { + if (this.mixer && this.animation) { + const time = performance.now() * 0.001; + this.instancedMeshes.forEach((mesh, meshIndex) => { + // Create a single action for each mesh if it doesn't exist + if (!this.animationActions.has(meshIndex)) { + const action = this.mixer!.clipAction(this.animation!); + this.animationActions.set(meshIndex, action); + } + + const action = this.animationActions.get(meshIndex)!; + action.play(); + + for (let i = 0; i < mesh.count; i++) { + this.mixer!.setTime(time + this.timeOffsets[i]); + mesh.setMorphAt(i, this.biomeMeshes[meshIndex]); + } + mesh.morphTexture!.needsUpdate = true; + }); + } + } +} diff --git a/landing/src/three/constants.ts b/landing/src/three/constants.ts new file mode 100644 index 000000000..c512c8890 --- /dev/null +++ b/landing/src/three/constants.ts @@ -0,0 +1,218 @@ +import { ResourceMiningTypes } from "@/types"; +import { BuildingType, RealmLevelNames, RealmLevels, ResourcesIds, StructureType } from "@bibliothecadao/eternum"; +import * as THREE from "three"; +import { BiomeType } from "./components/Biome"; + +export const HEX_SIZE = 1; +export const BUILDINGS_CENTER = [10, 10]; + +export const PREVIEW_BUILD_COLOR_VALID = 0x00a300; +export const PREVIEW_BUILD_COLOR_INVALID = 0xff0000; + +export const structureTypeToBuildingType: Record = { + [StructureType.Bank]: BuildingType.Bank, + [StructureType.Realm]: BuildingType.Castle, + [StructureType.FragmentMine]: BuildingType.FragmentMine, + [StructureType.Settlement]: BuildingType.Castle, + [StructureType.Hyperstructure]: BuildingType.Castle, +}; + +export const castleLevelToRealmCastle: Record = { + [RealmLevels.Settlement]: RealmLevelNames.Settlement, + [RealmLevels.City]: RealmLevelNames.City, + [RealmLevels.Kingdom]: RealmLevelNames.Kingdom, + [RealmLevels.Empire]: RealmLevelNames.Empire, +}; + +export const buildingModelPaths: Record = { + // placeholder for now + [BuildingType.None]: "/models/buildings-opt/farm.glb", + [BuildingType.Bank]: "/models/buildings-opt/bank.glb", + [BuildingType.ArcheryRange]: "/models/buildings-opt/archerrange.glb", + [BuildingType.Barracks]: "/models/buildings-opt/barracks.glb", + [BuildingType.Castle]: "/models/buildings-opt/castle1.glb", + [BuildingType.Farm]: "/models/buildings-opt/farm.glb", + [BuildingType.FishingVillage]: "/models/buildings-opt/fishery.glb", + [BuildingType.FragmentMine]: "/models/buildings-opt/mine.glb", + [BuildingType.Market]: "/models/buildings-opt/market.glb", + [BuildingType.Resource]: "/models/buildings-opt/mine.glb", + [BuildingType.Stable]: "/models/buildings-opt/stable.glb", + [BuildingType.Storehouse]: "/models/buildings-opt/storehouse.glb", + [BuildingType.TradingPost]: "/models/buildings-opt/market.glb", + [BuildingType.Walls]: "/models/buildings-opt/market.glb", + [BuildingType.WatchTower]: "/models/buildings-opt/market.glb", + [BuildingType.WorkersHut]: "/models/buildings-opt/workers_hut.glb", + [ResourceMiningTypes.Forge]: "/models/buildings-opt/forge.glb", + [ResourceMiningTypes.Mine]: "/models/buildings-opt/mine_2.glb", + [ResourceMiningTypes.LumberMill]: "/models/buildings-opt/lumber_mill.glb", + [ResourceMiningTypes.Dragonhide]: "/models/buildings-opt/dragonhide.glb", + [RealmLevelNames.Settlement]: "/models/buildings-opt/castle0.glb", + [RealmLevelNames.City]: "/models/buildings-opt/castle1.glb", + [RealmLevelNames.Kingdom]: "/models/buildings-opt/castle2.glb", + [RealmLevelNames.Empire]: "/models/buildings-opt/castle3.glb", +}; + +const BASE_PATH = "/models/biomes-opt/"; +export const biomeModelPaths: Record = { + Bare: BASE_PATH + "bare.glb", + Beach: BASE_PATH + "beach.glb", + TemperateDeciduousForest: BASE_PATH + "deciduousForest.glb", + DeepOcean: BASE_PATH + "deepOcean.glb", + Grassland: BASE_PATH + "grassland.glb", + Ocean: BASE_PATH + "ocean.glb", + Outline: BASE_PATH + "outline.glb", + Scorched: BASE_PATH + "scorched.glb", + Tundra: BASE_PATH + "tundra.glb", + TemperateDesert: BASE_PATH + "temperateDesert.glb", + Shrubland: BASE_PATH + "shrubland.glb", + Snow: BASE_PATH + "snow.glb", + Taiga: BASE_PATH + "taiga.glb", + TemperateRainForest: BASE_PATH + "temperateRainforest.glb", + SubtropicalDesert: BASE_PATH + "subtropicalDesert.glb", + TropicalRainForest: BASE_PATH + "tropicalRainforest.glb", + TropicalSeasonalForest: BASE_PATH + "tropicalSeasonalForest.glb", +}; + +export const PROGRESS_HALF_THRESHOLD = 0.5; +export const PROGRESS_FINAL_THRESHOLD = 1; + +export enum StructureProgress { + STAGE_1 = 0, + STAGE_2 = 1, + STAGE_3 = 2, +} + +export const StructureModelPaths: Record = { + [StructureType.Realm]: [ + "models/buildings-opt/castle0.glb", + "models/buildings-opt/castle1.glb", + "models/buildings-opt/castle2.glb", + "models/buildings-opt/castle3.glb", + ], + // Order follows StructureProgress + [StructureType.Hyperstructure]: [ + "models/buildings-opt/hyperstructure_init.glb", + "models/buildings-opt/hyperstructure_half.glb", + "models/buildings-opt/hyperstructure.glb", + ], + [StructureType.Bank]: ["/models/buildings-opt/bank.glb"], + [StructureType.FragmentMine]: ["models/buildings-opt/mine_2.glb"], + // placeholder for now + [StructureType.Settlement]: ["models/buildings-opt/castle2.glb"], +}; + +export const StructureLabelPaths: Record = { + [StructureType.Realm]: "textures/realm_label.png", + [StructureType.Hyperstructure]: "textures/hyper_label.png", + [StructureType.FragmentMine]: "textures/fragment_mine_label.png", + // placeholder for now + [StructureType.Bank]: "", + // placeholder for now + [StructureType.Settlement]: "textures/fragment_mine_label.png", +}; + +export const MinesMaterialsParams: Record< + number, + { color: THREE.Color; emissive: THREE.Color; emissiveIntensity: number } +> = { + // [ResourcesIds.Copper]: ResourceMiningTypes.Forge, + // [ResourcesIds.ColdIron]: ResourceMiningTypes.Forge, + // [ResourcesIds.Ignium]: ResourceMiningTypes.Forge, + // [ResourcesIds.Gold]: ResourceMiningTypes.Forge, + // [ResourcesIds.Silver]: ResourceMiningTypes.Forge, + // [ResourcesIds.AlchemicalSilver]: ResourceMiningTypes.Forge, + // [ResourcesIds.Adamantine]: ResourceMiningTypes.Forge, + [ResourcesIds.Copper]: { + color: new THREE.Color(0.86, 0.26, 0.0), + emissive: new THREE.Color(6.71, 0.25, 0.08), + emissiveIntensity: 5.9, + }, + [ResourcesIds.ColdIron]: { + color: new THREE.Color(0.69, 0.63, 0.99), + emissive: new THREE.Color(0.76, 1.63, 6.82), + emissiveIntensity: 5.9, + }, + [ResourcesIds.Ignium]: { + color: new THREE.Color(0.97, 0.03, 0.03), + emissive: new THREE.Color(6.31, 0.13, 0.04), + emissiveIntensity: 8.6, + }, + [ResourcesIds.Gold]: { + color: new THREE.Color(0.99, 0.83, 0.3), + emissive: new THREE.Color(9.88, 6.79, 3.02), + emissiveIntensity: 4.9, + }, + [ResourcesIds.Silver]: { + color: new THREE.Color(0.93, 0.93, 0.93), + emissive: new THREE.Color(3.55, 3.73, 5.51), + emissiveIntensity: 8.6, + }, + [ResourcesIds.AlchemicalSilver]: { + color: new THREE.Color(0.93, 0.93, 0.93), + emissive: new THREE.Color(1.87, 4.57, 9.33), + emissiveIntensity: 8.4, + }, + [ResourcesIds.Adamantine]: { + color: new THREE.Color(0.0, 0.27, 1.0), + emissive: new THREE.Color(1.39, 0.52, 8.16), + emissiveIntensity: 10, + }, + [ResourcesIds.Diamonds]: { + color: new THREE.Color(1.6, 1.47, 1.96), + emissive: new THREE.Color(0.8, 0.73, 5.93), + emissiveIntensity: 0.2, + }, + [ResourcesIds.Sapphire]: { + color: new THREE.Color(0.23, 0.5, 0.96), + emissive: new THREE.Color(0, 0, 5.01), + emissiveIntensity: 2.5, + }, + [ResourcesIds.Ruby]: { + color: new THREE.Color(0.86, 0.15, 0.15), + emissive: new THREE.Color(2.59, 0.0, 0.0), + emissiveIntensity: 4, + }, + [ResourcesIds.DeepCrystal]: { + color: new THREE.Color(1.21, 2.7, 3.27), + emissive: new THREE.Color(0.58, 0.77, 3), + emissiveIntensity: 5, + }, + [ResourcesIds.TwilightQuartz]: { + color: new THREE.Color(0.43, 0.16, 0.85), + emissive: new THREE.Color(0.0, 0.03, 4.25), + emissiveIntensity: 5.7, + }, + [ResourcesIds.EtherealSilica]: { + color: new THREE.Color(0.06, 0.73, 0.51), + emissive: new THREE.Color(0.0, 0.12, 0.0), + emissiveIntensity: 2, + }, + [ResourcesIds.Stone]: { + color: new THREE.Color(0.38, 0.38, 0.38), + emissive: new THREE.Color(0, 0, 0), + emissiveIntensity: 0, + }, + [ResourcesIds.Coal]: { + color: new THREE.Color(0.18, 0.18, 0.18), + emissive: new THREE.Color(0, 0, 0), + emissiveIntensity: 0, + }, + [ResourcesIds.Obsidian]: { + color: new THREE.Color(0.06, 0.06, 0.06), + emissive: new THREE.Color(0, 0, 0), + emissiveIntensity: 1, + }, + [ResourcesIds.TrueIce]: { + color: new THREE.Color(3.0, 3.0, 3.8), + emissive: new THREE.Color(1.0, 1.0, 1), + emissiveIntensity: 4, + }, + [ResourcesIds.AncientFragment]: { + color: new THREE.Color(0.43, 0.85, 0.16), + emissive: new THREE.Color(0.0, 3.25, 0.03), + emissiveIntensity: 5.7, + }, +}; + +export const LAND_NAME = "land"; +export const SMALL_DETAILS_NAME = "small_details"; diff --git a/landing/src/three/geometry/HexagonGeometry.ts b/landing/src/three/geometry/HexagonGeometry.ts new file mode 100644 index 000000000..75149c28a --- /dev/null +++ b/landing/src/three/geometry/HexagonGeometry.ts @@ -0,0 +1,31 @@ +import * as THREE from "three"; +import { HEX_SIZE } from "../constants"; + +export const createHexagonShape = (radius: number) => { + const shape = new THREE.Shape(); + for (let i = 0; i < 6; i++) { + // Adjust the angle to start the first point at the top + const angle = (Math.PI / 3) * i - Math.PI / 2; + const x = radius * Math.cos(angle); + const y = radius * Math.sin(angle); + if (i === 0) { + shape.moveTo(x, y); + } else { + shape.lineTo(x, y); + } + } + shape.closePath(); + + return shape; +}; + +const edgesGeometry = new THREE.EdgesGeometry(new THREE.ShapeGeometry(createHexagonShape(HEX_SIZE))); +const edgesMaterial = new THREE.LineBasicMaterial({ + color: "black", + linewidth: 1, + transparent: true, + opacity: 0.15, +}); + +const hexagonEdgeMesh = new THREE.LineSegments(edgesGeometry, edgesMaterial); +hexagonEdgeMesh.rotateX(Math.PI / 2); diff --git a/landing/src/three/helpers/utils.ts b/landing/src/three/helpers/utils.ts new file mode 100644 index 000000000..78135f98f --- /dev/null +++ b/landing/src/three/helpers/utils.ts @@ -0,0 +1,92 @@ +import { HexPosition, ResourceMiningTypes } from "@/types"; +import { ResourcesIds } from "@bibliothecadao/eternum"; +import * as THREE from "three"; +import { DRACOLoader, GLTFLoader, MeshoptDecoder } from "three-stdlib"; +import { HEX_SIZE } from "../constants"; + +const dracoLoader = new DRACOLoader(); +dracoLoader.setDecoderPath("https://www.gstatic.com/draco/versioned/decoders/1.5.7/"); +dracoLoader.preload(); + +export const gltfLoader = new GLTFLoader(); +gltfLoader.setDRACOLoader(dracoLoader); +gltfLoader.setMeshoptDecoder(MeshoptDecoder()); + +export const getHexagonCoordinates = ( + instancedMesh: THREE.InstancedMesh, + instanceId: number, +): { hexCoords: HexPosition; position: THREE.Vector3 } => { + const matrix = new THREE.Matrix4(); + instancedMesh.getMatrixAt(instanceId, matrix); + const position = new THREE.Vector3(); + matrix.decompose(position, new THREE.Quaternion(), new THREE.Vector3()); + + const hexCoords = getHexForWorldPosition(position); + + return { hexCoords, position }; +}; + +export const getWorldPositionForHex = (hexCoords: HexPosition, flat: boolean = true) => { + const hexRadius = HEX_SIZE; + const hexHeight = hexRadius * 2; + const hexWidth = Math.sqrt(3) * hexRadius; + const vertDist = hexHeight * 0.75; + const horizDist = hexWidth; + + const col = hexCoords.col; + const row = hexCoords.row; + const rowOffset = ((row % 2) * Math.sign(row) * horizDist) / 2; + const x = col * horizDist - rowOffset; + const z = row * vertDist; + const y = flat ? 0 : pseudoRandom(x, z) * 2; + return new THREE.Vector3(x, y, z); +}; + +export const getHexForWorldPosition = (worldPosition: { x: number; y: number; z: number }): HexPosition => { + const hexRadius = HEX_SIZE; + const hexHeight = hexRadius * 2; + const hexWidth = Math.sqrt(3) * hexRadius; + const vertDist = hexHeight * 0.75; + const horizDist = hexWidth; + + const row = Math.round(worldPosition.z / vertDist); + // hexception offsets hack + const rowOffset = ((row % 2) * Math.sign(row) * horizDist) / 2; + const col = Math.round((worldPosition.x + rowOffset) / horizDist); + + return { + col, + row, + }; +}; + +const pseudoRandom = (x: number, y: number) => { + const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453123; + return n - Math.floor(n); +}; + +export const ResourceIdToMiningType: Partial > = { + [ResourcesIds.Copper]: ResourceMiningTypes.Forge, + [ResourcesIds.ColdIron]: ResourceMiningTypes.Forge, + [ResourcesIds.Ignium]: ResourceMiningTypes.Forge, + [ResourcesIds.Gold]: ResourceMiningTypes.Forge, + [ResourcesIds.Silver]: ResourceMiningTypes.Forge, + [ResourcesIds.Diamonds]: ResourceMiningTypes.Mine, + [ResourcesIds.Sapphire]: ResourceMiningTypes.Mine, + [ResourcesIds.Ruby]: ResourceMiningTypes.Mine, + [ResourcesIds.DeepCrystal]: ResourceMiningTypes.Mine, + [ResourcesIds.TwilightQuartz]: ResourceMiningTypes.Mine, + [ResourcesIds.EtherealSilica]: ResourceMiningTypes.Mine, + [ResourcesIds.Stone]: ResourceMiningTypes.Mine, + [ResourcesIds.Coal]: ResourceMiningTypes.Mine, + [ResourcesIds.Obsidian]: ResourceMiningTypes.Mine, + [ResourcesIds.TrueIce]: ResourceMiningTypes.Mine, + [ResourcesIds.Wood]: ResourceMiningTypes.LumberMill, + [ResourcesIds.Hartwood]: ResourceMiningTypes.LumberMill, + [ResourcesIds.Ironwood]: ResourceMiningTypes.LumberMill, + [ResourcesIds.Mithral]: ResourceMiningTypes.Forge, + [ResourcesIds.Dragonhide]: ResourceMiningTypes.Dragonhide, + [ResourcesIds.AlchemicalSilver]: ResourceMiningTypes.Forge, + [ResourcesIds.Adamantine]: ResourceMiningTypes.Forge, + [ResourcesIds.AncientFragment]: ResourceMiningTypes.Mine, +}; diff --git a/landing/src/types/index.ts b/landing/src/types/index.ts index 184089dc3..5523c0007 100644 --- a/landing/src/types/index.ts +++ b/landing/src/types/index.ts @@ -355,3 +355,12 @@ export interface LiveAuctions { end_timestamp: number; metadata?: TokenMetadata; } + +export enum ResourceMiningTypes { + Forge = "forge", + Mine = "mine", + LumberMill = "lumber_mill", + Dragonhide = "dragonhide", +} + +export type HexPosition = { col: number; row: number }; diff --git a/landing/tsconfig.app.json b/landing/tsconfig.app.json index d91ce19d4..09b8e4e40 100644 --- a/landing/tsconfig.app.json +++ b/landing/tsconfig.app.json @@ -28,7 +28,8 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"] - } + }, + "types": ["vite-plugin-svgr/client"] }, "include": ["src"] } diff --git a/landing/tsconfig.app.tsbuildinfo b/landing/tsconfig.app.tsbuildinfo index 5ba5a6185..d185eee51 100644 --- a/landing/tsconfig.app.tsbuildinfo +++ b/landing/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/layouts/dashboard-layout.tsx","./src/components/modules/animated-grid.tsx","./src/components/modules/app-sidebar.tsx","./src/components/modules/bridge.tsx","./src/components/modules/cartridge-connect-button.tsx","./src/components/modules/data-card.tsx","./src/components/modules/data.tsx","./src/components/modules/filters.tsx","./src/components/modules/mode-toggle.tsx","./src/components/modules/realm-card.tsx","./src/components/modules/realm-mint-dialog.tsx","./src/components/modules/realms-grid.tsx","./src/components/modules/season-pass-mint-dialog.tsx","./src/components/modules/season-pass-row.tsx","./src/components/modules/season-pass.tsx","./src/components/modules/select-nft-actions.tsx","./src/components/modules/sidebar.tsx","./src/components/modules/swap-panel.tsx","./src/components/modules/swap.tsx","./src/components/modules/top-navigation.tsx","./src/components/providers/Starknet.tsx","./src/components/providers/cartridge-controller.tsx","./src/components/providers/theme-provider.tsx","./src/components/typography/type-h1.tsx","./src/components/typography/type-h2.tsx","./src/components/typography/type-h3.tsx","./src/components/typography/type-h4.tsx","./src/components/typography/type-p.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-select.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/dojo/createSystemCalls.ts","./src/dojo/setup.ts","./src/dojo/setupNetwork.ts","./src/dojo/world.ts","./src/hooks/use-mobile.tsx","./src/hooks/useAccountOrBurner.tsx","./src/hooks/useCollectionTokens.tsx","./src/hooks/useMintSeasonPass.tsx","./src/hooks/useMintTestRealm.tsx","./src/hooks/useNftSelection.tsx","./src/hooks/context/DojoContext.tsx","./src/hooks/gql/execute.ts","./src/hooks/gql/fragment-masking.ts","./src/hooks/gql/gql.ts","./src/hooks/gql/graphql.ts","./src/hooks/gql/index.ts","./src/hooks/query/players.tsx","./src/hooks/query/realms.tsx","./src/lib/utils.ts","./src/lib/ark/getCollectionTokens.ts","./src/routes/__root.tsx","./src/routes/bridge.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/mint.lazy.tsx","./src/routes/my-empire.tsx","./src/routes/passes.lazy.tsx","./src/routes/trade.lazy.tsx","./src/stories/button.stories.tsx","./src/stories/dashboard-layout.stories.tsx","./src/stories/data-card.stories.tsx","./src/stories/season-pass-row.stories.tsx","./src/stories/season-pass.stories.tsx","./src/stories/swap-panel.stories.tsx","./src/stories/swap.stories.tsx","./src/stories/top-navigation.stories.tsx","./src/types/index.ts"],"errors":true,"version":"5.6.3"} \ No newline at end of file +{"root":["./src/config.ts","./src/main.tsx","./src/routetree.gen.ts","./src/vite-env.d.ts","./src/components/layouts/dashboard-layout.tsx","./src/components/modules/animated-grid.tsx","./src/components/modules/app-sidebar.tsx","./src/components/modules/bridge.tsx","./src/components/modules/cartridge-connect-button.tsx","./src/components/modules/data-card.tsx","./src/components/modules/data.tsx","./src/components/modules/filters.tsx","./src/components/modules/lords-purchase-dialog.tsx","./src/components/modules/mode-toggle.tsx","./src/components/modules/realm-card.tsx","./src/components/modules/realm-mint-dialog.tsx","./src/components/modules/realms-grid.tsx","./src/components/modules/season-pass-mint-dialog.tsx","./src/components/modules/season-pass-row.tsx","./src/components/modules/season-pass.tsx","./src/components/modules/select-nft-actions.tsx","./src/components/modules/swap-panel.tsx","./src/components/modules/swap.tsx","./src/components/modules/token-balance.tsx","./src/components/modules/top-navigation.tsx","./src/components/providers/starknet.tsx","./src/components/providers/theme-provider.tsx","./src/components/typography/type-h1.tsx","./src/components/typography/type-h2.tsx","./src/components/typography/type-h3.tsx","./src/components/typography/type-h4.tsx","./src/components/typography/type-p.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-select.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/dojo/createsystemcalls.ts","./src/dojo/setup.ts","./src/dojo/setupnetwork.ts","./src/hooks/use-debounce.tsx","./src/hooks/use-lords.tsx","./src/hooks/use-mobile.tsx","./src/hooks/useaccountorburner.tsx","./src/hooks/usecollectiontokens.tsx","./src/hooks/usemintseasonpass.tsx","./src/hooks/useminttestrealm.tsx","./src/hooks/usenftselection.tsx","./src/hooks/context/dojocontext.tsx","./src/hooks/gql/execute.ts","./src/hooks/gql/fragment-masking.ts","./src/hooks/gql/gql.ts","./src/hooks/gql/graphql.ts","./src/hooks/gql/index.ts","./src/hooks/query/players.tsx","./src/hooks/query/realms.tsx","./src/lib/utils.ts","./src/lib/ark/getcollectiontokens.ts","./src/routes/__root.tsx","./src/routes/bridge.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/mint.lazy.tsx","./src/routes/my-empire.tsx","./src/routes/passes.lazy.tsx","./src/routes/trade.lazy.tsx","./src/stories/button.stories.tsx","./src/stories/dashboard-layout.stories.tsx","./src/stories/data-card.stories.tsx","./src/stories/season-pass-row.stories.tsx","./src/stories/season-pass.stories.tsx","./src/stories/swap-panel.stories.tsx","./src/stories/swap.stories.tsx","./src/stories/top-navigation.stories.tsx","./src/types/index.ts"],"errors":true,"version":"5.6.3"} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65221b6ec..6c433be82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,13 +109,13 @@ importers: version: 1.0.0-alpha.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(starknet@6.11.0(encoding@0.1.13))(typescript@5.6.3) '@dojoengine/react': specifier: 1.0.0-alpha.28 - version: 1.0.0-alpha.28(@types/node@20.17.1)(@types/react@18.3.12)(@vitest/ui@2.1.3(vitest@2.1.3))(jsdom@24.1.3)(react@18.3.1)(starknet@6.11.0(encoding@0.1.13))(terser@5.36.0)(type-fest@2.19.0)(typescript@5.6.3)(zod@3.23.8) + version: 1.0.0-alpha.28(@types/node@20.17.1)(@types/react@18.3.12)(@vitest/ui@2.1.3)(jsdom@24.1.3)(react@18.3.1)(starknet@6.11.0(encoding@0.1.13))(terser@5.36.0)(type-fest@2.19.0)(typescript@5.6.3)(zod@3.23.8) '@dojoengine/recs': specifier: ^2.0.13 version: 2.0.13(typescript@5.6.3)(zod@3.23.8) '@dojoengine/state': specifier: 1.0.0-alpha.28 - version: 1.0.0-alpha.28(@types/node@20.17.1)(@vitest/ui@2.1.3(vitest@2.1.3))(jsdom@24.1.3)(starknet@6.11.0(encoding@0.1.13))(terser@5.36.0)(typescript@5.6.3)(zod@3.23.8) + version: 1.0.0-alpha.28(@types/node@20.17.1)(@vitest/ui@2.1.3)(jsdom@24.1.3)(starknet@6.11.0(encoding@0.1.13))(terser@5.36.0)(typescript@5.6.3)(zod@3.23.8) '@dojoengine/torii-client': specifier: 1.0.0-alpha.28 version: 1.0.0-alpha.28 @@ -232,7 +232,7 @@ importers: version: 0.20.5(@vite-pwa/assets-generator@0.2.6)(vite@5.4.10(@types/node@20.17.1)(terser@5.36.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0) vitest-canvas-mock: specifier: ^0.3.3 - version: 0.3.3(vitest@2.1.3(@types/node@20.17.1)(@vitest/ui@2.1.3)(jsdom@24.1.3)(terser@5.36.0)) + version: 0.3.3(vitest@2.1.3) wouter: specifier: ^2.12.1 version: 2.12.1(react@18.3.1) @@ -269,7 +269,7 @@ importers: version: 4.3.3(vite@5.4.10(@types/node@20.17.1)(terser@5.36.0)) '@vitest/coverage-v8': specifier: ^2.0.5 - version: 2.1.3(vitest@2.1.3(@types/node@20.17.1)(@vitest/ui@2.1.3)(jsdom@24.1.3)(terser@5.36.0)) + version: 2.1.3(vitest@2.1.3) '@vitest/ui': specifier: ^2.0.1 version: 2.1.3(vitest@2.1.3) @@ -389,6 +389,9 @@ importers: '@ark-project/react': specifier: 2.0.0-beta.2 version: 2.0.0-beta.2(encoding@0.1.13)(typescript@5.6.3)(viem@2.21.45(typescript@5.6.3)(zod@3.23.8)) + '@avnu/avnu-sdk': + specifier: 2.1.1 + version: 2.1.1(ethers@6.13.4)(qs@6.13.0)(starknet@6.11.0(encoding@0.1.13)) '@bibliothecadao/eternum': specifier: workspace:^ version: link:../sdk/packages/eternum @@ -509,6 +512,9 @@ importers: nuqs: specifier: ^2.0.4 version: 2.1.1(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + postprocessing: + specifier: ^6.36.2 + version: 6.36.3(three@0.166.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -527,6 +533,12 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.14) + three: + specifier: ^0.166.1 + version: 0.166.1 + three-stdlib: + specifier: ^2.29.5 + version: 2.33.0(three@0.166.1) viem: specifier: ^2.21.45 version: 2.21.45(typescript@5.6.3)(zod@3.23.8) @@ -600,6 +612,9 @@ importers: '@types/react-dom': specifier: ^18.3.0 version: 18.3.1 + '@types/three': + specifier: ^0.163.0 + version: 0.163.0 '@vitejs/plugin-react': specifier: ^4.3.2 version: 4.3.3(vite@5.4.10(@types/node@20.17.1)(terser@5.36.0)) @@ -706,6 +721,9 @@ packages: '@adraffy/ens-normalize@1.10.0': resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==} + '@adraffy/ens-normalize@1.10.1': + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + '@adraffy/ens-normalize@1.11.0': resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} @@ -748,6 +766,14 @@ packages: '@ark-project/react@2.0.0-beta.2': resolution: {integrity: sha512-QmnL2Y5ondgYpMWaIcm8nva7/7HPKonHg7QrAXIsVU2N63pwbCbK6sNrksPoZ4PKUFgPUBRbF7/kOwKvwG/uKA==} + '@avnu/avnu-sdk@2.1.1': + resolution: {integrity: sha512-y/r/pVT2pU33fGHNVE7A5UIAqQhjEXYQhUh7EodY1s5H7mhRd5U8zHOtI5z6vmpuSnUv0hSvOmmgz8HTuwZ7ew==} + engines: {node: '>=18'} + peerDependencies: + ethers: ^6.11.1 + qs: ^6.12.0 + starknet: ^6.6.0 + '@babel/code-frame@7.26.0': resolution: {integrity: sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==} engines: {node: '>=6.9.0'} @@ -4207,6 +4233,9 @@ packages: '@types/node@20.17.1': resolution: {integrity: sha512-j2VlPv1NnwPJbaCNv69FO/1z4lId0QmGvpT41YxitRtWlg96g/j8qcv2RKsLKe2F6OJgyXhupN1Xo17b2m139Q==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/node@22.8.1': resolution: {integrity: sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==} @@ -4610,6 +4639,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + agent-base@7.1.1: resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} engines: {node: '>= 14'} @@ -5827,6 +5859,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + ethers@6.13.4: + resolution: {integrity: sha512-21YtnZVg4/zKkCQPjrDj38B1r4nQvTZLopUGMLQ1ePU2zV/joCfDC3t3iKQjWRzjjjbzR+mdAIoikeBRNkdllA==} + engines: {node: '>=14.0.0'} + eval@0.1.8: resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} engines: {node: '>= 0.8'} @@ -8829,6 +8865,9 @@ packages: tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tslib@2.8.0: resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==} @@ -9480,6 +9519,18 @@ packages: utf-8-validate: optional: true + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -9587,6 +9638,8 @@ snapshots: '@adraffy/ens-normalize@1.10.0': {} + '@adraffy/ens-normalize@1.10.1': {} + '@adraffy/ens-normalize@1.11.0': {} '@alloc/quick-lru@5.2.0': {} @@ -9660,6 +9713,12 @@ snapshots: - typescript - viem + '@avnu/avnu-sdk@2.1.1(ethers@6.13.4)(qs@6.13.0)(starknet@6.11.0(encoding@0.1.13))': + dependencies: + ethers: 6.13.4 + qs: 6.13.0 + starknet: 6.11.0(encoding@0.1.13) + '@babel/code-frame@7.26.0': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -10660,10 +10719,10 @@ snapshots: - utf-8-validate - zod - '@dojoengine/react@1.0.0-alpha.28(@types/node@20.17.1)(@types/react@18.3.12)(@vitest/ui@2.1.3(vitest@2.1.3))(jsdom@24.1.3)(react@18.3.1)(starknet@6.11.0(encoding@0.1.13))(terser@5.36.0)(type-fest@2.19.0)(typescript@5.6.3)(zod@3.23.8)': + '@dojoengine/react@1.0.0-alpha.28(@types/node@20.17.1)(@types/react@18.3.12)(@vitest/ui@2.1.3)(jsdom@24.1.3)(react@18.3.1)(starknet@6.11.0(encoding@0.1.13))(terser@5.36.0)(type-fest@2.19.0)(typescript@5.6.3)(zod@3.23.8)': dependencies: '@dojoengine/recs': 2.0.13(typescript@5.6.3)(zod@3.23.8) - '@dojoengine/state': 1.0.0-alpha.28(@types/node@20.17.1)(@vitest/ui@2.1.3(vitest@2.1.3))(jsdom@24.1.3)(starknet@6.11.0(encoding@0.1.13))(terser@5.36.0)(typescript@5.6.3)(zod@3.23.8) + '@dojoengine/state': 1.0.0-alpha.28(@types/node@20.17.1)(@vitest/ui@2.1.3)(jsdom@24.1.3)(starknet@6.11.0(encoding@0.1.13))(terser@5.36.0)(typescript@5.6.3)(zod@3.23.8) '@dojoengine/torii-client': 1.0.0-alpha.28 '@dojoengine/utils': 1.0.0-alpha.28(starknet@6.11.0(encoding@0.1.13))(typescript@5.6.3)(zod@3.23.8) '@latticexyz/utils': 2.2.14 @@ -10715,7 +10774,7 @@ snapshots: '@dojoengine/recs': 2.0.13(typescript@5.6.3)(zod@3.23.8) '@dojoengine/torii-client': 1.0.0 starknet: 6.11.0(encoding@0.1.13) - vitest: 1.6.0(@types/node@20.17.1)(@vitest/ui@2.1.3(vitest@2.1.3))(jsdom@24.1.3)(terser@5.36.0) + vitest: 1.6.0(@types/node@20.17.1)(@vitest/ui@2.1.3)(jsdom@24.1.3)(terser@5.36.0) transitivePeerDependencies: - '@edge-runtime/vm' - '@types/node' @@ -10736,12 +10795,12 @@ snapshots: - utf-8-validate - zod - '@dojoengine/state@1.0.0-alpha.28(@types/node@20.17.1)(@vitest/ui@2.1.3(vitest@2.1.3))(jsdom@24.1.3)(starknet@6.11.0(encoding@0.1.13))(terser@5.36.0)(typescript@5.6.3)(zod@3.23.8)': + '@dojoengine/state@1.0.0-alpha.28(@types/node@20.17.1)(@vitest/ui@2.1.3)(jsdom@24.1.3)(starknet@6.11.0(encoding@0.1.13))(terser@5.36.0)(typescript@5.6.3)(zod@3.23.8)': dependencies: '@dojoengine/recs': 2.0.13(typescript@5.6.3)(zod@3.23.8) '@dojoengine/torii-client': 1.0.0-alpha.28 starknet: 6.11.0(encoding@0.1.13) - vitest: 1.6.0(@types/node@20.17.1)(@vitest/ui@2.1.3(vitest@2.1.3))(jsdom@24.1.3)(terser@5.36.0) + vitest: 1.6.0(@types/node@20.17.1)(@vitest/ui@2.1.3)(jsdom@24.1.3)(terser@5.36.0) transitivePeerDependencies: - '@edge-runtime/vm' - '@types/node' @@ -13727,6 +13786,10 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + '@types/node@22.8.1': dependencies: undici-types: 6.19.8 @@ -14132,7 +14195,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@2.1.3(vitest@2.1.3(@types/node@20.17.1)(@vitest/ui@2.1.3)(jsdom@24.1.3)(terser@5.36.0))': + '@vitest/coverage-v8@2.1.3(vitest@2.1.3)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -14312,6 +14375,8 @@ snapshots: acorn@8.13.0: {} + aes-js@4.0.0-beta.5: {} + agent-base@7.1.1: dependencies: debug: 4.3.7 @@ -15838,6 +15903,19 @@ snapshots: etag@1.8.1: {} + ethers@6.13.4: + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + eval@0.1.8: dependencies: '@types/node': 20.17.1 @@ -19405,6 +19483,8 @@ snapshots: tslib@2.6.3: {} + tslib@2.7.0: {} + tslib@2.8.0: {} tsup@8.3.5(@swc/core@1.7.40)(jiti@2.3.3)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0): @@ -19966,12 +20046,12 @@ snapshots: fsevents: 2.3.3 terser: 5.36.0 - vitest-canvas-mock@0.3.3(vitest@2.1.3(@types/node@20.17.1)(@vitest/ui@2.1.3)(jsdom@24.1.3)(terser@5.36.0)): + vitest-canvas-mock@0.3.3(vitest@2.1.3): dependencies: jest-canvas-mock: 2.5.2 vitest: 2.1.3(@types/node@20.17.1)(@vitest/ui@2.1.3)(jsdom@24.1.3)(terser@5.36.0) - vitest@1.6.0(@types/node@20.17.1)(@vitest/ui@2.1.3(vitest@2.1.3))(jsdom@24.1.3)(terser@5.36.0): + vitest@1.6.0(@types/node@20.17.1)(@vitest/ui@2.1.3)(jsdom@24.1.3)(terser@5.36.0): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -20402,6 +20482,8 @@ snapshots: ws@8.13.0: {} + ws@8.17.1: {} + ws@8.18.0: {} xml-name-validator@5.0.0: {}