import { Asset } from 'expo-asset'; import { GLView } from 'expo-gl'; import { Renderer } from 'expo-three'; import React, { useEffect, useRef, useState } from 'react'; import { Dimensions, PanResponder, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import background1 from '@/assets/images/background-1.jpg'; const { width, height } = Dimensions.get('window'); interface HexagonTile { position: THREE.Vector3; mesh?: THREE.Mesh | null; stroke?: THREE.Mesh | null; occupied: boolean; unit?: THREE.Mesh; biome: 'desert' | 'grass' | 'water' | 'forest' | 'rock'; modelPath?: string; } interface Unit { id: string; name: string; color: number; cost: number; level: number; } export default function TFTGameScene() { const sceneRef = useRef(null); const cameraRef = useRef(null); const rendererRef = useRef(null); const tilesRef = useRef([]); const animationFrameRef = useRef(null); const [selectedUnit, setSelectedUnit] = useState(null); const [gold, setGold] = useState(10); const [isDragging, setIsDragging] = useState(false); const [dragStartPosition, setDragStartPosition] = useState({ x: 0, y: 0 }); const [draggedTile, setDraggedTile] = useState<{ row: number; col: number } | null>(null); const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 }); const [draggedUnitMesh, setDraggedUnitMesh] = useState(null); const [availableUnits, setAvailableUnits] = useState([ { id: '1', name: 'Warrior', color: 0xff6b6b, cost: 3, level: 1 }, { id: '2', name: 'Mage', color: 0x4ecdc4, cost: 4, level: 1 }, { id: '3', name: 'Archer', color: 0x45b7d1, cost: 2, level: 1 }, { id: '4', name: 'Tank', color: 0x96ceb4, cost: 5, level: 1 }, { id: '5', name: 'Assassin', color: 0xfeca57, cost: 3, level: 1 }, { id: '6', name: 'Knight', color: 0x9b59b6, cost: 4, level: 1 }, { id: '7', name: 'Priest', color: 0xe74c3c, cost: 2, level: 1 }, { id: '8', name: 'Wizard', color: 0x3498db, cost: 3, level: 1 }, { id: '9', name: 'Ranger', color: 0x2ecc71, cost: 2, level: 1 }, { id: '10', name: 'Guardian', color: 0xf39c12, cost: 4, level: 1 }, { id: '11', name: 'Berserker', color: 0xe67e22, cost: 3, level: 1 }, { id: '12', name: 'Sorcerer', color: 0x8e44ad, cost: 5, level: 1 }, { id: '13', name: 'Scout', color: 0x16a085, cost: 2, level: 1 }, { id: '14', name: 'Paladin', color: 0xc0392b, cost: 4, level: 1 }, { id: '15', name: 'Necromancer', color: 0x7f8c8d, cost: 3, level: 1 }, { id: '16', name: 'Dragon', color: 0xe74c3c, cost: 5, level: 1 }, ]); // GLB model loader const gltfLoader = new GLTFLoader(); // Load GLB model with proper asset handling const loadGLBModel = async (modelPath: string): Promise => { try { // Use static require for GLB models let asset; switch (modelPath) { case 'hex_sand_detail.gltf.glb': asset = Asset.fromModule(require('../assets/arena/hex_sand_detail.gltf.glb')); break; case 'hex_forest.gltf.glb': asset = Asset.fromModule(require('../assets/arena/hex_forest.gltf.glb')); break; case 'hex_water_detail.gltf.glb': asset = Asset.fromModule(require('../assets/arena/hex_water_detail.gltf.glb')); break; case 'hex_rock_detail.gltf.glb': asset = Asset.fromModule(require('../assets/arena/hex_rock_detail.gltf.glb')); break; case 'hex_sand_roadA.gltf.glb': asset = Asset.fromModule(require('../assets/arena/hex_sand_roadA.gltf.glb')); break; default: console.log('Model not found:', modelPath); return null; } await asset.downloadAsync(); return new Promise((resolve, reject) => { gltfLoader.load( asset.uri, (gltf) => { const model = gltf.scene; // Scale to fit within hexagon (radius = 0.5) model.scale.set(0.4, 0.4, 0.4); // Rotate model to align with our hexagons (vertical) model.rotation.y = Math.PI / 6; // 30 degrees to align with our hexagons // Position underneath hexagon with minimal gap model.position.y = -0.01; // Minimal gap resolve(model); }, undefined, (error) => { console.error('Error loading GLB model:', error); reject(error); } ); }); } catch (error) { console.error('Error loading GLB model:', error); return null; } }; // Create enhanced geometry for different biomes const createEnhancedGeometry = (biome: string, radius: number = 0.5) => { switch (biome) { case 'desert': // Create sand-like geometry with bumps const sandGeometry = new THREE.CylinderGeometry(radius, radius, 0.1, 6); const sandMaterial = new THREE.MeshLambertMaterial({ color: 0xd2b48c, transparent: true, opacity: 0.9 }); return new THREE.Mesh(sandGeometry, sandMaterial); case 'water': // Create water-like geometry with waves const waterGeometry = new THREE.CylinderGeometry(radius, radius, 0.08, 6); const waterMaterial = new THREE.MeshLambertMaterial({ color: 0x87CEEB, transparent: true, opacity: 0.7 }); return new THREE.Mesh(waterGeometry, waterMaterial); case 'forest': // Create forest-like geometry with texture const forestGeometry = new THREE.CylinderGeometry(radius, radius, 0.12, 6); const forestMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22, transparent: true, opacity: 0.9 }); return new THREE.Mesh(forestGeometry, forestMaterial); case 'rock': // Create rock-like geometry with roughness const rockGeometry = new THREE.CylinderGeometry(radius, radius, 0.15, 6); const rockMaterial = new THREE.MeshLambertMaterial({ color: 0x696969, transparent: true, opacity: 0.9 }); return new THREE.Mesh(rockGeometry, rockMaterial); default: // Default grass geometry const grassGeometry = new THREE.CylinderGeometry(radius, radius, 0.1, 6); const grassMaterial = new THREE.MeshLambertMaterial({ color: 0x90EE90, transparent: true, opacity: 0.9 }); return new THREE.Mesh(grassGeometry, grassMaterial); } }; // Hexagon geometry helper const createHexagonGeometry = (radius: number, height: number) => { const geometry = new THREE.CylinderGeometry(radius, radius, height, 6); return geometry; }; // Create a single hexagon tile with GLB models const createHexagonTile = async (x: number, z: number, radius: number = 0.5, biome: 'desert' | 'grass' | 'water' | 'forest' | 'rock' = 'grass') => { // Biome-based model paths let modelPath: string | undefined; switch (biome) { case 'desert': modelPath = 'hex_sand_detail.gltf.glb'; break; case 'grass': modelPath = 'hex_forest.gltf.glb'; break; case 'water': modelPath = 'hex_water_detail.gltf.glb'; break; case 'forest': modelPath = 'hex_forest.gltf.glb'; break; case 'rock': modelPath = 'hex_rock_detail.gltf.glb'; break; default: modelPath = 'hex_forest.gltf.glb'; } return { position: new THREE.Vector3(x, 0, z), mesh: null, // We don't need our custom mesh anymore stroke: null, // We don't need our custom stroke anymore occupied: false, biome, modelPath }; }; // Create a unit cube with proper level scaling const createUnitCube = (color: number = 0xff6b6b, level: number = 1) => { const geometry = new THREE.BoxGeometry(0.3, 0.3, 0.3); const material = new THREE.MeshLambertMaterial({ color }); const mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.5; // Position above GLB models // TFT style scaling: Level 1 = normal, Level 2 = 1.2x, Level 3 = 1.4x, Level 4 = 1.6x const scaleMultiplier = 1 + (level - 1) * 0.2; mesh.scale.set(scaleMultiplier, scaleMultiplier, scaleMultiplier); return mesh; }; // Create the beautiful 6x6 hexagonal arena with GLB models const createArena = async () => { const tiles: HexagonTile[][] = []; const hexRadius = 0.5; const hexWidth = hexRadius * 2; const hexHeight = hexRadius * Math.sqrt(3); // Calculate arena center offset to center it on screen const arenaWidth = 5 * hexWidth * 0.75; // 6 columns const arenaHeight = 5 * hexHeight; // 6 rows const centerX = -arenaWidth / 2; const centerZ = -arenaHeight / 2; // Define biome layout based on the image with roads const biomeLayout = [ ['desert', 'desert', 'desert', 'grass', 'grass', 'grass'], ['desert', 'desert', 'grass', 'grass', 'grass', 'grass'], ['desert', 'grass', 'grass', 'grass', 'grass', 'water'], ['grass', 'grass', 'grass', 'forest', 'water', 'water'], ['grass', 'grass', 'forest', 'water', 'water', 'water'], ['grass', 'forest', 'water', 'water', 'water', 'water'] ]; // Define road layout (connecting different biomes) const roadLayout = [ [false, false, false, false, false, false], [false, false, true, false, false, false], [false, true, false, true, false, false], [false, false, true, false, true, false], [false, false, false, true, false, true], [false, false, false, false, true, false] ]; for (let row = 0; row < 6; row++) { tiles[row] = []; for (let col = 0; col < 6; col++) { // Calculate position with offset for hexagonal grid, centered const x = centerX + col * hexWidth * 0.75; const z = centerZ + row * hexHeight + (col % 2) * (hexHeight / 2); // Get biome from layout const biome = biomeLayout[row][col] as 'desert' | 'grass' | 'water' | 'forest' | 'rock'; const hasRoad = roadLayout[row][col]; const tile = await createHexagonTile(x, z, hexRadius, biome); tiles[row][col] = tile; if (sceneRef.current) { // Only add GLB model - no custom hexagons if (tile.modelPath) { try { const model = await loadGLBModel(tile.modelPath); if (model) { // Center the model with the hexagon position model.position.copy(tile.position); model.position.y = 0.0; // At ground level // Add color tint based on area (player vs enemy) const isPlayerArea = row >= 3; // Bottom 3 rows are player area if (isPlayerArea) { // Player area - normal colors model.children.forEach(child => { if (child instanceof THREE.Mesh && child.material) { if (Array.isArray(child.material)) { child.material.forEach(mat => { if (mat instanceof THREE.MeshLambertMaterial) { mat.emissive = new THREE.Color(0x00ff00); mat.emissiveIntensity = 0.1; } }); } else if (child.material instanceof THREE.MeshLambertMaterial) { child.material.emissive = new THREE.Color(0x00ff00); child.material.emissiveIntensity = 0.1; } } }); } else { // Enemy area - red tint model.children.forEach(child => { if (child instanceof THREE.Mesh && child.material) { if (Array.isArray(child.material)) { child.material.forEach(mat => { if (mat instanceof THREE.MeshLambertMaterial) { mat.emissive = new THREE.Color(0xff0000); mat.emissiveIntensity = 0.2; } }); } else if (child.material instanceof THREE.MeshLambertMaterial) { child.material.emissive = new THREE.Color(0xff0000); child.material.emissiveIntensity = 0.2; } } }); } sceneRef.current.add(model); } } catch (error) { console.log('Using fallback geometry for tile'); } } } } } tilesRef.current = tiles; // Add bases to arena corners (outside the arena) - REMOVED // if (sceneRef.current) { // // Player base (bottom right corner) - Castle style // const playerBaseGeometry = new THREE.CylinderGeometry(1.5, 1.5, 0.4, 8); // const playerBaseMaterial = new THREE.MeshLambertMaterial({ // color: 0x4a90e2, // transparent: true, // opacity: 0.9 // }); // const playerBase = new THREE.Mesh(playerBaseGeometry, playerBaseMaterial); // playerBase.position.set(centerX + 6 * hexWidth * 0.75, 0.2, centerZ + 6 * hexHeight); // sceneRef.current.add(playerBase); // // // Enemy base (top left corner) - Tower style // const enemyBaseGeometry = new THREE.CylinderGeometry(1.2, 1.2, 0.6, 8); // const enemyBaseMaterial = new THREE.MeshLambertMaterial({ // color: 0x666666, // transparent: true, // opacity: 0.9 // }); // const enemyBase = new THREE.Mesh(enemyBaseGeometry, enemyBaseMaterial); // enemyBase.position.set(centerX - hexWidth * 0.75, 0.3, centerZ - hexHeight); // sceneRef.current.add(enemyBase); // // // Add some decorative structures // addDecorations(centerX, centerZ, hexWidth, hexHeight); // } }; // Add decorative structures to the arena const addDecorations = async (centerX: number, centerZ: number, hexWidth: number, hexHeight: number) => { if (!sceneRef.current) return; // Add a castle in the center grass area const castleGeometry = new THREE.BoxGeometry(1.2, 1.5, 1.2); const castleMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); const castle = new THREE.Mesh(castleGeometry, castleMaterial); castle.position.set(centerX + 2 * hexWidth * 0.75, 0.75, centerZ + 2 * hexHeight); sceneRef.current.add(castle); // Add castle towers for (let i = 0; i < 4; i++) { const towerGeometry = new THREE.CylinderGeometry(0.2, 0.2, 0.8, 8); const towerMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); const tower = new THREE.Mesh(towerGeometry, towerMaterial); const angle = (i * Math.PI) / 2; tower.position.set( centerX + 2 * hexWidth * 0.75 + Math.cos(angle) * 0.8, 0.4, centerZ + 2 * hexHeight + Math.sin(angle) * 0.8 ); sceneRef.current.add(tower); } // Add a windmill in the water area const windmillGeometry = new THREE.CylinderGeometry(0.3, 0.3, 1.0, 8); const windmillMaterial = new THREE.MeshLambertMaterial({ color: 0xDEB887 }); const windmill = new THREE.Mesh(windmillGeometry, windmillMaterial); windmill.position.set(centerX + 4 * hexWidth * 0.75, 0.5, centerZ + 4 * hexHeight); sceneRef.current.add(windmill); // Add windmill blades const bladeGeometry = new THREE.BoxGeometry(0.1, 0.8, 0.05); const bladeMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); for (let i = 0; i < 4; i++) { const blade = new THREE.Mesh(bladeGeometry, bladeMaterial); const angle = (i * Math.PI) / 2; blade.position.set( centerX + 4 * hexWidth * 0.75 + Math.cos(angle) * 0.4, 1.0, centerZ + 4 * hexHeight + Math.sin(angle) * 0.4 ); blade.rotation.y = angle; sceneRef.current.add(blade); } // Add some trees in the forest area using GLB models for (let i = 0; i < 3; i++) { try { const treeModel = await loadGLBModel('hex_forest.gltf.glb'); if (treeModel) { treeModel.position.set( centerX + (3 + i) * hexWidth * 0.75, 0.4, centerZ + (4 + i % 2) * hexHeight ); treeModel.scale.set(0.3, 0.3, 0.3); sceneRef.current.add(treeModel); } else { // Fallback to geometry const treeGeometry = new THREE.ConeGeometry(0.3, 0.8, 8); const treeMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 }); const tree = new THREE.Mesh(treeGeometry, treeMaterial); tree.position.set( centerX + (3 + i) * hexWidth * 0.75, 0.4, centerZ + (4 + i % 2) * hexHeight ); sceneRef.current.add(tree); } } catch (error) { console.log('Tree model not loaded, using fallback'); const treeGeometry = new THREE.ConeGeometry(0.3, 0.8, 8); const treeMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 }); const tree = new THREE.Mesh(treeGeometry, treeMaterial); tree.position.set( centerX + (3 + i) * hexWidth * 0.75, 0.4, centerZ + (4 + i % 2) * hexHeight ); sceneRef.current.add(tree); } } // Add rocks in the desert area using GLB models for (let i = 0; i < 2; i++) { try { const rockModel = await loadGLBModel('hex_rock_detail.gltf.glb'); if (rockModel) { rockModel.position.set( centerX + i * hexWidth * 0.75, 0.1, centerZ + i * hexHeight ); rockModel.scale.set(0.2, 0.2, 0.2); sceneRef.current.add(rockModel); } else { // Fallback to geometry const rockGeometry = new THREE.DodecahedronGeometry(0.2); const rockMaterial = new THREE.MeshLambertMaterial({ color: 0x696969 }); const rock = new THREE.Mesh(rockGeometry, rockMaterial); rock.position.set( centerX + i * hexWidth * 0.75, 0.1, centerZ + i * hexHeight ); sceneRef.current.add(rock); } } catch (error) { console.log('Rock model not loaded, using fallback'); const rockGeometry = new THREE.DodecahedronGeometry(0.2); const rockMaterial = new THREE.MeshLambertMaterial({ color: 0x696969 }); const rock = new THREE.Mesh(rockGeometry, rockMaterial); rock.position.set( centerX + i * hexWidth * 0.75, 0.1, centerZ + i * hexHeight ); sceneRef.current.add(rock); } } // Add a bridge over water const bridgeGeometry = new THREE.BoxGeometry(1.0, 0.1, 0.3); const bridgeMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); const bridge = new THREE.Mesh(bridgeGeometry, bridgeMaterial); bridge.position.set(centerX + 3 * hexWidth * 0.75, 0.15, centerZ + 4 * hexHeight); sceneRef.current.add(bridge); // Add some houses in the grass area for (let i = 0; i < 2; i++) { const houseGeometry = new THREE.BoxGeometry(0.6, 0.4, 0.6); const houseMaterial = new THREE.MeshLambertMaterial({ color: 0xDEB887 }); const house = new THREE.Mesh(houseGeometry, houseMaterial); house.position.set( centerX + (1 + i) * hexWidth * 0.75, 0.2, centerZ + (3 + i) * hexHeight ); sceneRef.current.add(house); // Add roof const roofGeometry = new THREE.ConeGeometry(0.4, 0.3, 4); const roofMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); const roof = new THREE.Mesh(roofGeometry, roofMaterial); roof.position.set( centerX + (1 + i) * hexWidth * 0.75, 0.55, centerZ + (3 + i) * hexHeight ); roof.rotation.y = Math.PI / 4; sceneRef.current.add(roof); } }; // Handle unit purchase const handleUnitPurchase = (unit: Unit) => { if (gold >= unit.cost) { setGold(gold - unit.cost); setSelectedUnit(unit); console.log('Unit selected:', unit.name); // Debug log } }; // Handle tile placement with proper merge logic const handleTilePlacement = (row: number, col: number) => { if (selectedUnit && tilesRef.current[row] && tilesRef.current[row][col]) { const tile = tilesRef.current[row][col]; if (!tile.occupied) { // Check for merge first const wasMerged = checkForMerge(selectedUnit); if (!wasMerged) { // Place new unit normally - add to scene directly since we don't have tile.mesh const unit = createUnitCube(selectedUnit.color, selectedUnit.level); unit.position.copy(tile.position); unit.position.y = 0.5; // Position above GLB model if (sceneRef.current) { sceneRef.current.add(unit); } tile.occupied = true; tile.unit = unit; } setSelectedUnit(null); } } }; // Add some sample units to the arena (only in user area) const addSampleUnits = () => { const sampleUnits = [ { row: 4, col: 2, unit: availableUnits[0] }, // Warrior { row: 5, col: 3, unit: availableUnits[1] }, // Mage { row: 4, col: 4, unit: availableUnits[2] }, // Archer ]; sampleUnits.forEach(({ row, col, unit }) => { if (tilesRef.current[row] && tilesRef.current[row][col]) { const tile = tilesRef.current[row][col]; const mesh = createUnitCube(unit.color, unit.level); mesh.position.copy(tile.position); mesh.position.y = 0.5; // Position above GLB model if (sceneRef.current) { sceneRef.current.add(mesh); } tile.occupied = true; tile.unit = mesh; } }); }; // Get tile at screen position const getTileAtPosition = (x: number, y: number) => { if (!cameraRef.current) return null; const mouse = new THREE.Vector2(); mouse.x = (x / width) * 2 - 1; mouse.y = -(y / height) * 2 + 1; const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, cameraRef.current); // Get all GLB models from the scene const modelMeshes: THREE.Mesh[] = []; if (sceneRef.current) { sceneRef.current.children.forEach(child => { if (child.type === 'Group') { child.children.forEach(groupChild => { if (groupChild.type === 'Mesh') { modelMeshes.push(groupChild as THREE.Mesh); } }); } }); } const intersects = raycaster.intersectObjects(modelMeshes); if (intersects.length > 0) { const intersectedMesh = intersects[0].object; // Find the tile based on the intersected mesh position for (let row = 0; row < 6; row++) { for (let col = 0; col < 6; col++) { const tile = tilesRef.current[row][col]; // Check if the intersected mesh belongs to this tile's position if (tile.position.distanceTo(intersectedMesh.position) < 0.5) { return { row, col, tile }; } } } } return null; }; // PanResponder for drag and drop const panResponder = PanResponder.create({ onStartShouldSetPanResponder: () => true, onMoveShouldSetPanResponder: () => true, onPanResponderGrant: (event) => { const touch = event.nativeEvent; console.log('Drag started at:', touch.locationX, touch.locationY); // Debug log setDragStartPosition({ x: touch.locationX, y: touch.locationY }); setDragPosition({ x: touch.locationX, y: touch.locationY }); setIsDragging(true); // Check if we're dragging an existing unit const tileInfo = getTileAtPosition(touch.locationX, touch.locationY); console.log('Tile info:', tileInfo); // Debug log if (tileInfo && tileInfo.tile.occupied && tileInfo.row >= 3) { console.log('Dragging existing unit'); // Debug log // Dragging an existing unit from user area setDraggedTile({ row: tileInfo.row, col: tileInfo.col }); setSelectedUnit(null); // Clear selected unit since we're moving existing one // Create a copy of the unit for dragging if (tileInfo.tile.unit) { const unitGeometry = new THREE.BoxGeometry(0.3, 0.3, 0.3); const unitMaterial = new THREE.MeshLambertMaterial({ color: tileInfo.tile.unit.material instanceof THREE.MeshLambertMaterial ? tileInfo.tile.unit.material.color : 0xff6b6b, transparent: true, opacity: 0.8 }); const dragMesh = new THREE.Mesh(unitGeometry, unitMaterial); dragMesh.position.y = 0.5; dragMesh.scale.copy(tileInfo.tile.unit.scale); // Copy the scale (level) setDraggedUnitMesh(dragMesh); // Add to scene immediately if (sceneRef.current) { sceneRef.current.add(dragMesh); } // Hide the original unit temporarily tileInfo.tile.unit.visible = false; } } else if (selectedUnit) { console.log('Dragging new unit:', selectedUnit.name); // Debug log // Dragging a new unit from bottom panel setDraggedTile(null); // Create a new unit mesh for dragging const unitGeometry = new THREE.BoxGeometry(0.3, 0.3, 0.3); const unitMaterial = new THREE.MeshLambertMaterial({ color: selectedUnit.color, transparent: true, opacity: 0.8 }); const dragMesh = new THREE.Mesh(unitGeometry, unitMaterial); dragMesh.position.y = 0.5; // Apply proper level scaling const scaleMultiplier = 1 + (selectedUnit.level - 1) * 0.2; dragMesh.scale.set(scaleMultiplier, scaleMultiplier, scaleMultiplier); setDraggedUnitMesh(dragMesh); // Add to scene immediately if (sceneRef.current) { sceneRef.current.add(dragMesh); } } }, onPanResponderMove: (event) => { if (!isDragging) return; const touch = event.nativeEvent; setDragPosition({ x: touch.locationX, y: touch.locationY }); const tileInfo = getTileAtPosition(touch.locationX, touch.locationY); // Update dragged unit position to follow touch if (draggedUnitMesh && cameraRef.current) { const mouse = new THREE.Vector2(); mouse.x = (touch.locationX / width) * 2 - 1; mouse.y = -(touch.locationY / height) * 2 + 1; const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, cameraRef.current); // Get intersection with a plane at y=0 const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); const intersection = new THREE.Vector3(); raycaster.ray.intersectPlane(plane, intersection); draggedUnitMesh.position.set(intersection.x, 0.5, intersection.z); } }, onPanResponderRelease: (event) => { if (!isDragging) return; const touch = event.nativeEvent; console.log('Drag ended at:', touch.locationX, touch.locationY); // Debug log const tileInfo = getTileAtPosition(touch.locationX, touch.locationY); console.log('Final tile info:', tileInfo); // Debug log if (draggedTile) { console.log('Moving existing unit'); // Debug log // Moving an existing unit if (tileInfo && !tileInfo.tile.occupied && tileInfo.row >= 3) { console.log('Moving unit to new position'); // Debug log // Move unit to new position const sourceTile = tilesRef.current[draggedTile.row][draggedTile.col]; const targetTile = tilesRef.current[tileInfo.row][tileInfo.col]; if (sourceTile.unit) { // Show the original unit again sourceTile.unit.visible = true; // Remove unit from scene if (sceneRef.current) { sceneRef.current.remove(sourceTile.unit); } sourceTile.occupied = false; // Add unit to new position sourceTile.unit.position.copy(targetTile.position); sourceTile.unit.position.y = 0.5; if (sceneRef.current) { sceneRef.current.add(sourceTile.unit); } targetTile.occupied = true; targetTile.unit = sourceTile.unit; // Clear source tile unit reference sourceTile.unit = undefined; } } else { console.log('Reset dragged unit'); // Debug log // Reset the dragged unit if placement failed const sourceTile = tilesRef.current[draggedTile.row][draggedTile.col]; if (sourceTile.unit) { sourceTile.unit.visible = true; } } setDraggedTile(null); } else if (selectedUnit) { console.log('Placing new unit:', selectedUnit.name); // Debug log // Placing a new unit if (tileInfo && !tileInfo.tile.occupied && tileInfo.row >= 3) { console.log('Placing unit at:', tileInfo.row, tileInfo.col); // Debug log // Place new unit normally - add to scene directly since we don't have tile.mesh const unit = createUnitCube(selectedUnit.color, selectedUnit.level); unit.position.copy(tileInfo.tile.position); unit.position.y = 0.5; // Position above GLB model if (sceneRef.current) { sceneRef.current.add(unit); } tileInfo.tile.occupied = true; tileInfo.tile.unit = unit; setSelectedUnit(null); } } // Remove dragged unit mesh from scene if (draggedUnitMesh && sceneRef.current) { sceneRef.current.remove(draggedUnitMesh); } setDraggedUnitMesh(null); setIsDragging(false); }, onPanResponderTerminate: (event) => { if (!isDragging) return; const touch = event.nativeEvent; const tileInfo = getTileAtPosition(touch.locationX, touch.locationY); if (draggedTile) { // Moving an existing unit if (tileInfo && !tileInfo.tile.occupied && tileInfo.row >= 3) { // Move unit to new position const sourceTile = tilesRef.current[draggedTile.row][draggedTile.col]; const targetTile = tilesRef.current[tileInfo.row][tileInfo.col]; if (sourceTile.unit) { // Show the original unit again sourceTile.unit.visible = true; // Remove unit from scene if (sceneRef.current) { sceneRef.current.remove(sourceTile.unit); } sourceTile.occupied = false; // Add unit to new position sourceTile.unit.position.copy(targetTile.position); sourceTile.unit.position.y = 0.5; if (sceneRef.current) { sceneRef.current.add(sourceTile.unit); } targetTile.occupied = true; targetTile.unit = sourceTile.unit; // Clear source tile unit reference sourceTile.unit = undefined; } } else { // Reset the dragged unit if placement failed const sourceTile = tilesRef.current[draggedTile.row][draggedTile.col]; if (sourceTile.unit) { sourceTile.unit.visible = true; } } setDraggedTile(null); } else if (selectedUnit) { // Placing a new unit if (tileInfo && !tileInfo.tile.occupied && tileInfo.row >= 3) { // Place new unit normally - add to scene directly since we don't have tile.mesh const unit = createUnitCube(selectedUnit.color, selectedUnit.level); unit.position.copy(tileInfo.tile.position); unit.position.y = 0.5; // Position above GLB model if (sceneRef.current) { sceneRef.current.add(unit); } tileInfo.tile.occupied = true; tileInfo.tile.unit = unit; setSelectedUnit(null); } } // Remove dragged unit mesh from scene if (draggedUnitMesh && sceneRef.current) { sceneRef.current.remove(draggedUnitMesh); } setDraggedUnitMesh(null); setIsDragging(false); }, }); // Setup scene const setupScene = async (gl: any) => { // Create renderer const renderer = new Renderer({ gl }); rendererRef.current = renderer; // Create scene with beautiful background const scene = new THREE.Scene(); scene.background = new THREE.Color(0x1a1a2e); // Beautiful dark blue for TFT theme sceneRef.current = scene; // Load and set background image try { const bgAsset = Asset.fromModule(background1); await bgAsset.downloadAsync(); console.log('Asset URI:', bgAsset.uri); // Create a new texture loader with more aggressive settings const textureLoader = new THREE.TextureLoader(); // Try multiple approaches to load the texture const loadTexture = () => { textureLoader.load( bgAsset.uri, (texture) => { console.log('Background texture loaded successfully'); if (sceneRef.current) { sceneRef.current.background = texture; } }, (progress) => { console.log('Loading progress:', progress); }, (error) => { console.log('Error loading background texture:', error); // Try again with a different approach setTimeout(() => { console.log('Retrying texture load...'); textureLoader.load( bgAsset.uri, (texture) => { console.log('Background texture loaded on retry'); if (sceneRef.current) { sceneRef.current.background = texture; } }, undefined, (retryError) => { console.log('Retry failed:', retryError); } ); }, 1000); } ); }; // Start loading loadTexture(); } catch (error) { console.log('Failed to load background image:', error); // Keep the default color } // Create fixed camera - positioned to show arena and bases const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); camera.position.set(0, 12, 12); // Closer to arena camera.lookAt(0, 0, 0); cameraRef.current = camera; // Add lighting const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9); directionalLight.position.set(5, 15, 10); scene.add(directionalLight); // Create arena with GLB models await createArena(); addSampleUnits(); // Animation loop - no camera movement const animate = () => { animationFrameRef.current = requestAnimationFrame(animate); renderer.render(scene, camera); gl.endFrameEXP(); }; animate(); }; // Cleanup on unmount useEffect(() => { return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, []); // Get units on arena with proper level detection const getUnitsOnArena = (): Unit[] => { const units: Unit[] = []; tilesRef.current.forEach(row => { row.forEach(tile => { if (tile.occupied && tile.unit) { const unitColor = (tile.unit.material as any)?.color?.getHexString(); const unitData = availableUnits.find(u => u.color.toString(16) === unitColor); if (unitData) { // Calculate level based on scale const scale = tile.unit.scale.x; const level = Math.round((scale - 1) / 0.2) + 1; units.push({ ...unitData, level: Math.max(1, Math.min(4, level)) }); } } }); }); return units; }; // Check for merging opportunities const checkForMerge = (newUnit: Unit) => { const arenaUnits = getUnitsOnArena(); const sameTypeUnits = arenaUnits.filter(u => u.name === newUnit.name); // TFT style: need 3 units of same type to merge if (sameTypeUnits.length >= 2) { // Calculate new level based on TFT rules const totalUnits = sameTypeUnits.length + 1; // +1 for the new unit const newLevel = Math.min(4, Math.floor(totalUnits / 3) + 1); // Create merged unit const mergedUnit: Unit = { ...newUnit, level: newLevel }; // Remove old units from arena let removedCount = 0; tilesRef.current.forEach(row => { row.forEach(tile => { if (tile.occupied && tile.unit && removedCount < 3) { const unitColor = (tile.unit.material as any)?.color?.getHexString(); const unitData = availableUnits.find(u => u.color.toString(16) === unitColor && u.name === newUnit.name); if (unitData) { // Remove unit from scene if (sceneRef.current) { sceneRef.current.remove(tile.unit); } tile.occupied = false; tile.unit = undefined; removedCount++; } } }); }); // Add merged unit to first empty tile for (let row = 3; row < 6; row++) { for (let col = 0; col < 6; col++) { const tile = tilesRef.current[row][col]; if (!tile.occupied) { const mergedMesh = createUnitCube(mergedUnit.color, mergedUnit.level); mergedMesh.position.copy(tile.position); // Position the merged unit mergedMesh.position.y = 0.5; // Position above GLB model if (sceneRef.current) { sceneRef.current.add(mergedMesh); } tile.occupied = true; tile.unit = mergedMesh; return true; // Merged successfully } } } } return false; // No merge }; return ( {/* Gold Display - Top Right */} 💰 {gold} {/* Horizontal Card Slider */} {/* Main Cards (4 visible) */} {availableUnits.slice(0, 4).map((unit, index) => { const isSelected = selectedUnit?.id === unit.id; return ( handleUnitPurchase(unit)} disabled={gold < unit.cost} > {/* Card Back Pattern */} {/* Unit Info Overlay */} {unit.name} ★ {unit.level} {unit.cost} ); })} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#1a1a2e', }, glView: { flex: 1, }, glContainer: { flex: 1, }, goldDisplay: { position: 'absolute', top: 50, right: 20, backgroundColor: 'rgba(0, 0, 0, 0.8)', paddingHorizontal: 15, paddingVertical: 10, borderRadius: 25, flexDirection: 'row', alignItems: 'center', shadowColor: '#feca57', shadowOffset: { width: 0, height: 0, }, shadowOpacity: 0.5, shadowRadius: 10, elevation: 10, }, goldIcon: { fontSize: 20, marginRight: 8, }, goldText: { color: '#feca57', fontSize: 18, fontWeight: 'bold', }, cardSlider: { position: 'absolute', bottom: 30, left: 0, right: 0, alignItems: 'center', }, sliderContainer: { flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', width: '100%', height: 100, }, mainCardsContainer: { flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', width: '100%', height: 100, }, unitCard: { padding: 12, borderRadius: 15, alignItems: 'center', minWidth: 70, height: 100, shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 8, borderWidth: 2, borderColor: 'rgba(255, 255, 255, 0.2)', backgroundColor: 'rgba(0, 0, 0, 0.8)', position: 'relative', marginHorizontal: 5, }, selectedUnitCard: { borderWidth: 3, borderColor: '#ffffff', shadowColor: '#ffffff', shadowOffset: { width: 0, height: 0, }, shadowOpacity: 0.8, shadowRadius: 15, elevation: 15, backgroundColor: 'rgba(255, 255, 255, 0.1)', }, unitName: { color: '#ffffff', fontSize: 12, fontWeight: 'bold', textAlign: 'center', }, unitLevel: { color: '#feca57', fontSize: 12, fontWeight: 'bold', marginTop: 2, }, unitCost: { color: '#feca57', fontSize: 16, fontWeight: 'bold', marginTop: 4, }, cardBack: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: 20, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center', zIndex: 1, }, cardPattern: { width: '100%', height: '100%', borderRadius: 20, backgroundColor: 'rgba(255, 255, 255, 0.1)', justifyContent: 'center', alignItems: 'center', }, eyePattern: { width: '80%', height: '80%', borderRadius: 40, backgroundColor: 'rgba(255, 255, 255, 0.2)', justifyContent: 'center', alignItems: 'center', }, eyeCenter: { width: '60%', height: '60%', borderRadius: 30, backgroundColor: 'rgba(255, 255, 255, 0.3)', }, eyeTriangle: { position: 'absolute', top: '50%', left: '50%', width: 0, height: 0, borderLeftWidth: 10, borderRightWidth: 10, borderBottomWidth: 20, borderStyle: 'solid', borderLeftColor: 'transparent', borderRightColor: 'transparent', borderBottomColor: 'transparent', transform: [{ rotate: '90deg' }], }, cardInfo: { position: 'absolute', bottom: 10, left: 0, right: 0, alignItems: 'center', zIndex: 2, }, levelContainer: { flexDirection: 'row', alignItems: 'center', marginTop: 2, }, });