1278 lines
44 KiB
TypeScript
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,
|
|
},
|
|
});
|