Files
tft-mobile/app/index.tsx
2025-08-04 02:15:53 +03:00

1278 lines
44 KiB
TypeScript

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<THREE.Scene | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const tilesRef = useRef<HexagonTile[][]>([]);
const animationFrameRef = useRef<number | null>(null);
const [selectedUnit, setSelectedUnit] = useState<Unit | null>(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<THREE.Mesh | null>(null);
const [availableUnits, setAvailableUnits] = useState<Unit[]>([
{ 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<THREE.Group | null> => {
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 (
<View style={styles.container}>
<View {...panResponder.panHandlers} style={styles.glContainer}>
<GLView style={styles.glView} onContextCreate={setupScene} />
</View>
{/* Gold Display - Top Right */}
<View style={styles.goldDisplay}>
<Text style={styles.goldIcon}>💰</Text>
<Text style={styles.goldText}>{gold}</Text>
</View>
{/* Horizontal Card Slider */}
<View style={styles.cardSlider}>
<View style={styles.sliderContainer}>
{/* Main Cards (4 visible) */}
<View style={styles.mainCardsContainer}>
{availableUnits.slice(0, 4).map((unit, index) => {
const isSelected = selectedUnit?.id === unit.id;
return (
<TouchableOpacity
key={unit.id}
style={[
styles.unitCard,
{
transform: [{ scale: isSelected ? 1.1 : 1.0 }],
zIndex: isSelected ? 10 : 1
},
isSelected && styles.selectedUnitCard
]}
onPress={() => handleUnitPurchase(unit)}
disabled={gold < unit.cost}
>
{/* Card Back Pattern */}
<View style={styles.cardBack}>
<View style={styles.cardPattern}>
<View style={styles.eyePattern}>
<View style={styles.eyeCenter} />
<View style={styles.eyeTriangle} />
</View>
</View>
</View>
{/* Unit Info Overlay */}
<View style={styles.cardInfo}>
<Text style={styles.unitName}>{unit.name}</Text>
<View style={styles.levelContainer}>
<Text style={styles.unitLevel}> {unit.level}</Text>
</View>
<Text style={styles.unitCost}>{unit.cost}</Text>
</View>
</TouchableOpacity>
);
})}
</View>
</View>
</View>
</View>
);
}
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,
},
});