commit 8da82f415090ac4e9eb9d20b09a5e5a34d607a48 Author: marti Date: Sat Dec 20 22:57:59 2025 +0100 Guardando estado actual diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbc6217 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.DS_Store +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b30b9bf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-alpine + +WORKDIR /app + +# Copiamos primero los ficheros de dependencias para aprovechar la caché de Docker +COPY package.json ./ + +# Instalamos dependencias +RUN npm install + +# Copiamos el resto del código +COPY . . + +# Exponemos el puerto de Vite +EXPOSE 5173 + +# Arrancamos en modo desarrollo +CMD ["npm", "run", "dev"] diff --git a/assets/images/inspiracio/Gemini_Generated_Image_90xtc790xtc790xt.png b/assets/images/inspiracio/Gemini_Generated_Image_90xtc790xtc790xt.png new file mode 100644 index 0000000..0e2cdf2 Binary files /dev/null and b/assets/images/inspiracio/Gemini_Generated_Image_90xtc790xtc790xt.png differ diff --git a/assets/images/inspiracio/Gemini_Generated_Image_9do1f79do1f79do1.png b/assets/images/inspiracio/Gemini_Generated_Image_9do1f79do1f79do1.png new file mode 100644 index 0000000..2850d7e Binary files /dev/null and b/assets/images/inspiracio/Gemini_Generated_Image_9do1f79do1f79do1.png differ diff --git a/assets/images/inspiracio/Gemini_Generated_Image_d6xjgxd6xjgxd6xj.png b/assets/images/inspiracio/Gemini_Generated_Image_d6xjgxd6xjgxd6xj.png new file mode 100644 index 0000000..6b351cd Binary files /dev/null and b/assets/images/inspiracio/Gemini_Generated_Image_d6xjgxd6xjgxd6xj.png differ diff --git a/assets/images/inspiracio/Gemini_Generated_Image_euqduceuqduceuqd.png b/assets/images/inspiracio/Gemini_Generated_Image_euqduceuqduceuqd.png new file mode 100644 index 0000000..df1ead4 Binary files /dev/null and b/assets/images/inspiracio/Gemini_Generated_Image_euqduceuqduceuqd.png differ diff --git a/assets/images/inspiracio/Gemini_Generated_Image_htupqthtupqthtup.png b/assets/images/inspiracio/Gemini_Generated_Image_htupqthtupqthtup.png new file mode 100644 index 0000000..628d120 Binary files /dev/null and b/assets/images/inspiracio/Gemini_Generated_Image_htupqthtupqthtup.png differ diff --git a/assets/images/inspiracio/Gemini_Generated_Image_l01f3bl01f3bl01f.png b/assets/images/inspiracio/Gemini_Generated_Image_l01f3bl01f3bl01f.png new file mode 100644 index 0000000..f429974 Binary files /dev/null and b/assets/images/inspiracio/Gemini_Generated_Image_l01f3bl01f3bl01f.png differ diff --git a/assets/images/inspiracio/Gemini_Generated_Image_nug34unug34unug3.png b/assets/images/inspiracio/Gemini_Generated_Image_nug34unug34unug3.png new file mode 100644 index 0000000..2a85bf9 Binary files /dev/null and b/assets/images/inspiracio/Gemini_Generated_Image_nug34unug34unug3.png differ diff --git a/assets/images/inspiracio/Gemini_Generated_Image_pfryrvpfryrvpfry.png b/assets/images/inspiracio/Gemini_Generated_Image_pfryrvpfryrvpfry.png new file mode 100644 index 0000000..4376a91 Binary files /dev/null and b/assets/images/inspiracio/Gemini_Generated_Image_pfryrvpfryrvpfry.png differ diff --git a/assets/images/inspiracio/Gemini_Generated_Image_pghwj0pghwj0pghw.png b/assets/images/inspiracio/Gemini_Generated_Image_pghwj0pghwj0pghw.png new file mode 100644 index 0000000..b4a32e2 Binary files /dev/null and b/assets/images/inspiracio/Gemini_Generated_Image_pghwj0pghwj0pghw.png differ diff --git a/assets/images/inspiracio/Gemini_Generated_Image_tsvxsxtsvxsxtsvx.png b/assets/images/inspiracio/Gemini_Generated_Image_tsvxsxtsvxsxtsvx.png new file mode 100644 index 0000000..c635899 Binary files /dev/null and b/assets/images/inspiracio/Gemini_Generated_Image_tsvxsxtsvxsxtsvx.png differ diff --git a/assets/images/inspiracio/Gemini_Generated_Image_uyw83buyw83buyw8.png b/assets/images/inspiracio/Gemini_Generated_Image_uyw83buyw83buyw8.png new file mode 100644 index 0000000..b44e1a6 Binary files /dev/null and b/assets/images/inspiracio/Gemini_Generated_Image_uyw83buyw83buyw8.png differ diff --git a/assets/images/inspiracio/entrada.jpg b/assets/images/inspiracio/entrada.jpg new file mode 100644 index 0000000..e58b2d8 Binary files /dev/null and b/assets/images/inspiracio/entrada.jpg differ diff --git a/assets/images/inspiracio/esquelet.jpg b/assets/images/inspiracio/esquelet.jpg new file mode 100644 index 0000000..f3d56e2 Binary files /dev/null and b/assets/images/inspiracio/esquelet.jpg differ diff --git a/assets/images/inspiracio/monstre1.jpg b/assets/images/inspiracio/monstre1.jpg new file mode 100644 index 0000000..6eb37b5 Binary files /dev/null and b/assets/images/inspiracio/monstre1.jpg differ diff --git a/assets/images/standees/barbaro.png b/assets/images/standees/barbaro.png new file mode 100644 index 0000000..b07e1f7 Binary files /dev/null and b/assets/images/standees/barbaro.png differ diff --git a/assets/images/standees/esqueleto.png b/assets/images/standees/esqueleto.png new file mode 100644 index 0000000..be2c8af Binary files /dev/null and b/assets/images/standees/esqueleto.png differ diff --git a/assets/images/standees/standee1.png b/assets/images/standees/standee1.png new file mode 100644 index 0000000..03236c5 Binary files /dev/null and b/assets/images/standees/standee1.png differ diff --git a/assets/images/standees/standee2.png b/assets/images/standees/standee2.png new file mode 100644 index 0000000..902f82a Binary files /dev/null and b/assets/images/standees/standee2.png differ diff --git a/assets/images/tiles/tile4x4.png b/assets/images/tiles/tile4x4.png new file mode 100644 index 0000000..3c799db Binary files /dev/null and b/assets/images/tiles/tile4x4.png differ diff --git a/assets/images/tiles/tile4x4_blue.png b/assets/images/tiles/tile4x4_blue.png new file mode 100644 index 0000000..34e2be4 Binary files /dev/null and b/assets/images/tiles/tile4x4_blue.png differ diff --git a/assets/images/tiles/tile4x4_orange.png b/assets/images/tiles/tile4x4_orange.png new file mode 100644 index 0000000..0efd33c Binary files /dev/null and b/assets/images/tiles/tile4x4_orange.png differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..05e9ccf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + app: + build: . + ports: + - "5173:5173" + volumes: + - .:/app + - /app/node_modules + environment: + - CHOKIDAR_USEPOLLING=true diff --git a/index.html b/index.html new file mode 100644 index 0000000..9a376a5 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Masmorres Isometric View + + +
+ + + diff --git a/manifest.md b/manifest.md new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..de7b408 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "masmorres-isometric", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^5.0.0" + }, + "dependencies": { + "three": "^0.160.0" + } +} \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..7f86076 --- /dev/null +++ b/src/main.js @@ -0,0 +1,475 @@ +import './style.css'; +import * as THREE from 'three'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; + +// --- CONFIGURACIÓN DE LA ESCENA --- +const CONFIG = { + CELL_SIZE: 2, // Unidades de Three.js por celda lógica + TILE_DIMENSIONS: 4, // Una tile es de 4x4 celdas +}; + +// --- ESTADO DEL JUEGO (DATA MODEL) --- +const ASSETS = { + tiles: { + 'tile_base': { src: '/assets/images/tiles/tile4x4.png', width: 4, height: 4 }, + 'tile_cyan': { src: '/assets/images/tiles/tile4x4_blue.png', width: 4, height: 4 }, + 'tile_orange': { src: '/assets/images/tiles/tile4x4_orange.png', width: 4, height: 4 }, + }, + standees: { + 'hero_1': { src: '/assets/images/standees/barbaro.png', height: 3 }, + 'hero_2': { src: '/assets/images/standees/esqueleto.png', height: 3 }, + } +}; + +const GAME_STATE = { + placedTiles: [ + { id: 1, type: 'tile_base', x: 0, y: 0 }, + { id: 2, type: 'tile_cyan', x: 0, y: -4 }, + { id: 3, type: 'tile_orange', x: -4, y: 0 } // Oeste de la primera + ], + entities: [ + { id: 101, type: 'hero_1', x: 1, y: 1 }, + { id: 102, type: 'hero_2', x: 2, y: -2 } + ] +}; + +// State de la sesión (UI) +const SESSION = { + selectedUnitId: null, + path: [], // Array de {x, y} + pathMeshes: [], // Array de meshes visuales + isAnimating: false // Flag para bloquear interacciones durante animación +}; + +// --- CONFIGURACIÓN BÁSICA THREE.JS --- +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0x202020); + +// Renderer +const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.shadowMap.enabled = true; +document.querySelector('#app').appendChild(renderer.domElement); + +// Cámara +const aspect = window.innerWidth / window.innerHeight; +const d = 15; +const camera = new THREE.OrthographicCamera(-d * aspect, d * aspect, d, -d, 1, 1000); +camera.position.set(20, 20, 20); +camera.lookAt(scene.position); + +// --- CONTROLES MODIFICADOS --- +// Roto con el ratón derecho, zoom con la rueda del ratón y si hago presión en la rueda, hago el paneo. +const controls = new OrbitControls(camera, renderer.domElement); +controls.enableDamping = true; +controls.dampingFactor = 0.05; +controls.screenSpacePanning = true; +controls.maxPolarAngle = Math.PI / 2; + +// Reasignación de botones +controls.mouseButtons = { + LEFT: null, // Dejamos el click izquierdo libre para nuestra lógica + MIDDLE: THREE.MOUSE.PAN, // Paneo con botón central/rueda + RIGHT: THREE.MOUSE.ROTATE // Rotación con derecho +}; +controls.zoomToCursor = true; // Zoom a donde apunta el ratón + +// Luces +const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); +scene.add(ambientLight); + +const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); +dirLight.position.set(10, 20, 5); +dirLight.castShadow = true; +scene.add(dirLight); + +const gridHelper = new THREE.GridHelper(40, 40, 0x444444, 0x111111); +scene.add(gridHelper); + +// Plano invisible para Raycasting en Y=0 +const planeGeometry = new THREE.PlaneGeometry(200, 200); +const planeMaterial = new THREE.MeshBasicMaterial({ visible: false }); +const raycastPlane = new THREE.Mesh(planeGeometry, planeMaterial); +raycastPlane.rotation.x = -Math.PI / 2; +scene.add(raycastPlane); + + +// --- HELPERS LÓGICOS --- +function worldToGrid(x, z) { + return { + x: Math.floor(x / CONFIG.CELL_SIZE), + y: Math.floor(z / CONFIG.CELL_SIZE) + }; +} + +function gridToWorld(gridX, gridY) { + return { + x: (gridX * CONFIG.CELL_SIZE) + (CONFIG.CELL_SIZE / 2), + z: (gridY * CONFIG.CELL_SIZE) + (CONFIG.CELL_SIZE / 2) + }; +} + +function isAdjacent(p1, p2) { + const dx = Math.abs(p1.x - p2.x); + const dy = Math.abs(p1.y - p2.y); + // Adyacencia ortogonal (cruz) + return (dx === 1 && dy === 0) || (dx === 0 && dy === 1); +} + +// --- CREACIÓN DE MARCADORES (CANVAS TEXTURE) --- +function createPathMarker(stepNumber) { + const canvas = document.createElement('canvas'); + canvas.width = 128; + canvas.height = 128; + const ctx = canvas.getContext('2d'); + + // Fondo Amarillo Semi-transparente + ctx.fillStyle = 'rgba(255, 255, 0, 0.5)'; + ctx.fillRect(0, 0, 128, 128); + + // Borde + ctx.strokeStyle = 'rgba(255, 200, 0, 0.8)'; + ctx.lineWidth = 10; + ctx.strokeRect(0, 0, 128, 128); + + // Número + ctx.fillStyle = '#000000'; + ctx.font = 'bold 60px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(stepNumber.toString(), 64, 64); + + const texture = new THREE.CanvasTexture(canvas); + // Importante para pixel art o gráficos nítidos, aunque aquí es texto + texture.minFilter = THREE.LinearFilter; + + // Crear el mesh + const geometry = new THREE.PlaneGeometry(CONFIG.CELL_SIZE * 0.9, CONFIG.CELL_SIZE * 0.9); + const material = new THREE.MeshBasicMaterial({ + map: texture, + transparent: true, + side: THREE.DoubleSide // Visible desde ambos lados + }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.rotation.x = -Math.PI / 2; + mesh.position.y = 0.05; // Ligeramente elevado sobre el suelo + return mesh; +} + +function updatePathVisuals() { + // 1. Limpiar anteriores + SESSION.pathMeshes.forEach(mesh => scene.remove(mesh)); + SESSION.pathMeshes = []; + + // 2. Crear nuevos + SESSION.path.forEach((pos, index) => { + const marker = createPathMarker(index + 1); + const worldPos = gridToWorld(pos.x, pos.y); + marker.position.x = worldPos.x; + marker.position.z = worldPos.z; + scene.add(marker); + SESSION.pathMeshes.push(marker); + }); +} + +// --- MANEJO VISUAL DE SELECCIÓN --- +function updateSelectionVisuals() { + GAME_STATE.entities.forEach(entity => { + if (!entity.mesh) return; + + if (entity.id === SESSION.selectedUnitId) { + // SELECCIONADO: Amarillo + Opacidad 50% + entity.mesh.material.color.setHex(0xffff00); + entity.mesh.material.opacity = 0.5; + entity.mesh.material.transparent = true; + } else { + // NO SELECCIONADO: Blanco (color original) + Opacidad 100% + entity.mesh.material.color.setHex(0xffffff); + entity.mesh.material.opacity = 1.0; + } + }); +} + +// --- ANIMACIÓN DE MOVIMIENTO --- +async function animateMovement() { + if (SESSION.path.length === 0 || !SESSION.selectedUnitId) return; + + SESSION.isAnimating = true; + + const unit = GAME_STATE.entities.find(e => e.id === SESSION.selectedUnitId); + if (!unit || !unit.mesh) { + SESSION.isAnimating = false; + return; + } + + // Copiar el path para ir consumiéndolo + const pathCopy = [...SESSION.path]; + + // Función helper para animar un solo paso + const animateStep = (targetGridPos) => { + return new Promise((resolve) => { + const startPos = { x: unit.mesh.position.x, z: unit.mesh.position.z }; + const targetWorldPos = gridToWorld(targetGridPos.x, targetGridPos.y); + const endPos = { x: targetWorldPos.x, z: targetWorldPos.z }; + + const duration = 300; // ms por paso + const startTime = Date.now(); + const standeeHeight = ASSETS.standees[unit.type].height; + + const hop = () => { + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + + // Easing suave (ease-in-out) + const eased = progress < 0.5 + ? 2 * progress * progress + : 1 - Math.pow(-2 * progress + 2, 2) / 2; + + // Interpolación lineal en X y Z + unit.mesh.position.x = startPos.x + (endPos.x - startPos.x) * eased; + unit.mesh.position.z = startPos.z + (endPos.z - startPos.z) * eased; + + // Saltito parabólico en Y + const hopHeight = 0.8; // Altura del salto + const hopProgress = Math.sin(progress * Math.PI); // 0 -> 1 -> 0 + unit.mesh.position.y = (standeeHeight / 2) + (hopProgress * hopHeight); + + if (progress < 1) { + requestAnimationFrame(hop); + } else { + // Asegurar posición final exacta + unit.mesh.position.x = endPos.x; + unit.mesh.position.z = endPos.z; + unit.mesh.position.y = standeeHeight / 2; + resolve(); + } + }; + + hop(); + }); + }; + + // Mover paso a paso + for (let i = 0; i < pathCopy.length; i++) { + const step = pathCopy[i]; + + // Animar el movimiento + await animateStep(step); + + // Actualizar posición lógica de la unidad + unit.x = step.x; + unit.y = step.y; + + // Borrar el marcador de esta celda (el primero del array) + SESSION.path.shift(); + updatePathVisuals(); + } + + // Al terminar, deseleccionar + SESSION.selectedUnitId = null; + updateSelectionVisuals(); + SESSION.isAnimating = false; +} + +// --- INTERACCIÓN --- +const raycaster = new THREE.Raycaster(); +const pointer = new THREE.Vector2(); + +window.addEventListener('pointerdown', (event) => { + // Bloquear interacciones durante animación + if (SESSION.isAnimating) return; + + // CLICK IZQUIERDO: Selección y Pathfinding + if (event.button === 0) { + + // Calcular coordenadas normalizadas (-1 a +1) + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(pointer, camera); + + // 1. Detectar Click en Entidades (Selección) + // Buscamos intersecciones con los meshes de las entidades + const entityMeshes = GAME_STATE.entities.map(e => e.mesh).filter(m => m); + const intersectsEntities = raycaster.intersectObjects(entityMeshes); + + if (intersectsEntities.length > 0) { + // Hemos clickado una entidad + const clickedMesh = intersectsEntities[0].object; + const entity = GAME_STATE.entities.find(e => e.mesh === clickedMesh); + if (entity) { + console.log("Seleccionado:", entity.type); + SESSION.selectedUnitId = entity.id; + SESSION.path = []; // Resetear camino + updatePathVisuals(); + updateSelectionVisuals(); // Actualizar color del standee + return; // Cortamos aquí para no procesar click de suelo a la vez + } + } + + // 2. Si hay unidad seleccionada, procesar Click en Suelo (Move) + if (SESSION.selectedUnitId) { + const intersectsGround = raycaster.intersectObject(raycastPlane); + + if (intersectsGround.length > 0) { + const point = intersectsGround[0].point; + const gridPos = worldToGrid(point.x, point.z); + + // LOGICA DEL PATHFINDING MANUAL + + // Punto de Origen: La última casilla del path, O la casilla de la unidad si empieza + let prevNode; + if (SESSION.path.length > 0) { + prevNode = SESSION.path[SESSION.path.length - 1]; + } else { + const unit = GAME_STATE.entities.find(e => e.id === SESSION.selectedUnitId); + prevNode = { x: unit.x, y: unit.y }; + } + + // A. Caso Deshacer (Click en la última) + if (SESSION.path.length > 0) { + const lastNode = SESSION.path[SESSION.path.length - 1]; + if (lastNode.x === gridPos.x && lastNode.y === gridPos.y) { + SESSION.path.pop(); // Borrar último + updatePathVisuals(); + return; + } + } + + // B. Caso Añadir (Tiene que ser adyacente al anterior) + if (isAdjacent(prevNode, gridPos)) { + // Comprobación opcional: Evitar bucles (no clickar en uno que ya está en el path) + const alreadyInPath = SESSION.path.some(p => p.x === gridPos.x && p.y === gridPos.y); + const isUnitPos = (gridPos.x === prevNode.x && gridPos.y === prevNode.y && SESSION.path.length === 0); + + if (!alreadyInPath && !isUnitPos) { + SESSION.path.push(gridPos); + updatePathVisuals(); + } + } + } + } + } + + // CLICK DERECHO: Ejecutar movimiento + if (event.button === 2) { + event.preventDefault(); // Evitar menú contextual + + if (SESSION.selectedUnitId && SESSION.path.length > 0) { + animateMovement(); + } + } +}); + +// Prevenir menú contextual del navegador +window.addEventListener('contextmenu', (event) => { + event.preventDefault(); +}); + + +// --- CARGA Y RENDERIZADO --- +const textureLoader = new THREE.TextureLoader(); + +function loadTexture(path) { + return new Promise((resolve) => { + textureLoader.load(path, (tex) => { + tex.colorSpace = THREE.SRGBColorSpace; + tex.magFilter = THREE.NearestFilter; + tex.minFilter = THREE.NearestFilter; + resolve(tex); + }); + }); +} + +async function initWorld() { + const tileTextures = {}; + const standeeTextures = {}; + + // Cargar Tiles + for (const [key, def] of Object.entries(ASSETS.tiles)) { + tileTextures[key] = await loadTexture(def.src); + } + // Cargar Standees + for (const [key, def] of Object.entries(ASSETS.standees)) { + standeeTextures[key] = await loadTexture(def.src); + } + + // Instanciar Tiles (Suelo) + GAME_STATE.placedTiles.forEach(tileData => { + const def = ASSETS.tiles[tileData.type]; + const tex = tileTextures[tileData.type]; + const worldWidth = def.width * CONFIG.CELL_SIZE; + const worldHeight = def.height * CONFIG.CELL_SIZE; + + const geometry = new THREE.PlaneGeometry(worldWidth, worldHeight); + const material = new THREE.MeshStandardMaterial({ + map: tex, + transparent: true, + side: THREE.DoubleSide + }); + const mesh = new THREE.Mesh(geometry, material); + + mesh.rotation.x = -Math.PI / 2; + mesh.receiveShadow = true; + + const originPos = gridToWorld(tileData.x, tileData.y); + + // Ajuste de centro + mesh.position.x = originPos.x + (worldWidth / 2) - (CONFIG.CELL_SIZE / 2); + mesh.position.z = originPos.z + (worldHeight / 2) - (CONFIG.CELL_SIZE / 2); + mesh.position.y = 0; + + scene.add(mesh); + }); + + // Instanciar Entidades + GAME_STATE.entities.forEach(entity => { + const def = ASSETS.standees[entity.type]; + const tex = standeeTextures[entity.type]; + + const imgAspect = tex.image.width / tex.image.height; + const height = def.height; + const width = height * imgAspect; + + const geometry = new THREE.PlaneGeometry(width, height); + const material = new THREE.MeshStandardMaterial({ + map: tex, + transparent: true, + alphaTest: 0.5, + side: THREE.DoubleSide + }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.castShadow = true; + + const pos = gridToWorld(entity.x, entity.y); + mesh.position.set(pos.x, height / 2, pos.z); + + scene.add(mesh); + entity.mesh = mesh; + }); +} + +initWorld(); + +function animate() { + requestAnimationFrame(animate); + controls.update(); + + // Billboard opcional para los marcadores de texto? + // No, los queremos pegados al suelo según la spec. + + renderer.render(scene, camera); +} +animate(); + +window.addEventListener('resize', () => { + const aspect = window.innerWidth / window.innerHeight; + camera.left = -d * aspect; + camera.right = d * aspect; + camera.top = d; + camera.bottom = -d; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +}); diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..e1f6713 --- /dev/null +++ b/src/style.css @@ -0,0 +1,25 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + color-scheme: light dark; + background-color: #242424; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; + overflow: hidden; /* Evitar scrollbars por el canvas */ +} + +#app { + width: 100%; + height: 100vh; +} + +canvas { + display: block; +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..d6b6724 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + // Permite que el servidor escuche en todas las interfaces de red (necesario para Docker) + host: true, + // Lista blanca de dominios permitidos + allowedHosts: [ + 'masmorres.martivich.es', + 'localhost', + '127.0.0.1' + ] + } +});