// ============================================ // CONSTANTS // ============================================ const TILE_WIDTH = 64; const TILE_HEIGHT = 32; const TILE_DEPTH = 16; // ============================================ // HAPTIC FEEDBACK // ============================================ function triggerHaptic() { if ('vibrate' in navigator) { navigator.vibrate(10); } } // ============================================ // THEME HANDLER // ============================================ function applyTheme(theme) { const body = document.body; if (theme.background.image) { body.style.backgroundImage = `url(${theme.background.image})`; } body.style.backgroundSize = theme.background.style || 'cover'; body.style.backgroundPosition = 'center'; body.style.backgroundRepeat = 'no-repeat'; body.style.backgroundAttachment = 'fixed'; const root = document.documentElement; Object.entries(theme.colors).forEach(([key, value]) => { const cssVar = `--${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`; root.style.setProperty(cssVar, value); }); console.log('🎨 Theme applied:', theme.name); } // ============================================ // TILE RENDERER // ============================================ function isoToScreen(gridX, gridY) { return { x: (gridX - gridY) * (TILE_WIDTH / 2) + window.innerWidth / 2, y: (gridX + gridY) * (TILE_HEIGHT / 2) + 100 }; } function createTile(tileData, theme) { const graphics = new PIXI.Graphics(); const pos = isoToScreen(tileData.x, tileData.y); const colors = { top: parseInt(theme.colors.tileTop.replace('#', '0x')), side: parseInt(theme.colors.tileSide.replace('#', '0x')), grid: parseInt(theme.colors.gridColor.replace('#', '0x')), highlight: parseInt(theme.colors.tileHighlight.replace('#', '0x')), gridHighlight: parseInt(theme.colors.gridHighlight.replace('#', '0x')) }; function drawNormalTile() { graphics.clear(); graphics.beginFill(colors.top); graphics.lineStyle(1, colors.grid); graphics.moveTo(pos.x, pos.y); graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.lineTo(pos.x, pos.y + TILE_HEIGHT); graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.lineTo(pos.x, pos.y); graphics.endFill(); graphics.beginFill(colors.side); graphics.lineStyle(1, colors.grid); graphics.moveTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.lineTo(pos.x, pos.y + TILE_HEIGHT); graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH); graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH); graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.endFill(); graphics.beginFill(colors.side); graphics.lineStyle(1, colors.grid); graphics.moveTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.lineTo(pos.x, pos.y + TILE_HEIGHT); graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH); graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH); graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.endFill(); } function drawHighlightTile() { graphics.clear(); graphics.beginFill(colors.highlight); graphics.lineStyle(2, colors.gridHighlight); graphics.moveTo(pos.x, pos.y); graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.lineTo(pos.x, pos.y + TILE_HEIGHT); graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.lineTo(pos.x, pos.y); graphics.endFill(); graphics.beginFill(colors.side); graphics.lineStyle(1, colors.gridHighlight); graphics.moveTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.lineTo(pos.x, pos.y + TILE_HEIGHT); graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH); graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH); graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.endFill(); graphics.beginFill(colors.side); graphics.lineStyle(1, colors.gridHighlight); graphics.moveTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.lineTo(pos.x, pos.y + TILE_HEIGHT); graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH); graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2 + TILE_DEPTH); graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2); graphics.endFill(); } drawNormalTile(); graphics.interactive = true; graphics.buttonMode = true; graphics.gridX = tileData.x; graphics.gridY = tileData.y; graphics.on('pointerover', () => { drawHighlightTile(); }); graphics.on('pointerout', () => { drawNormalTile(); }); return graphics; } function createPet(gridX, gridY, name = 'PET') { const container = new PIXI.Container(); const pos = isoToScreen(gridX, gridY); const body = new PIXI.Graphics(); body.beginFill(0xff6b9d); body.drawCircle(0, -30, 15); body.endFill(); body.beginFill(0xffffff); body.drawCircle(-5, -32, 4); body.drawCircle(5, -32, 4); body.endFill(); body.beginFill(0x000000); body.drawCircle(-5, -32, 2); body.drawCircle(5, -32, 2); body.endFill(); const nameText = new PIXI.Text(name, { fontFamily: 'Courier New', fontSize: 12, fill: '#ffffff', stroke: '#000000', strokeThickness: 2 }); nameText.anchor.set(0.5); nameText.y = -50; container.addChild(body); container.addChild(nameText); container.x = pos.x; container.y = pos.y; container.gridX = gridX; container.gridY = gridY; return container; } // ============================================ // GAME CLASS // ============================================ class Game { constructor() { this.worldId = ''; this.theme = null; this.world = null; this.app = null; this.pet = null; this.otherPlayers = new Map(); this.isMoving = false; this.dialogue = null; this.keys = {}; // Use global singleton 🌍 this.api = window.netNaviAPI; this.worldActions = null; this.playerInfo = { name: 'Guest', apiUrl: this.api.baseUrl }; } async init() { try { // Join world with callbacks 🌍 this.worldActions = this.api.joinWorld(this.worldId, { onWorldLoaded: (data) => { this.world = data.world; this.theme = data.theme; applyTheme(this.theme); this.initializeRenderer(); }, onCurrentPlayers: (players) => { players.forEach(player => { if (player.socketId !== this.api.worldSocket.id) { this.addOtherPlayer(player); } }); }, onPlayerJoined: (player) => { this.addOtherPlayer(player); }, onPlayerMoved: (data) => { const sprite = this.otherPlayers.get(data.socketId); if (sprite) { this.moveOtherPlayer(sprite, data.x, data.y); } }, onPlayerLeft: (data) => { const sprite = this.otherPlayers.get(data.socketId); if (sprite) { this.app.stage.removeChild(sprite); this.otherPlayers.delete(data.socketId); } }, onError: (error) => { console.error('❌ World error:', error); } }); console.log('✨ Game initializing...'); } catch (error) { console.error('❌ Failed to initialize game:', error); } } addOtherPlayer(player) { const sprite = createPet(player.x, player.y, player.name); sprite.alpha = 0.7; this.otherPlayers.set(player.socketId, sprite); this.app.stage.addChild(sprite); } moveOtherPlayer(sprite, targetX, targetY) { const targetPos = isoToScreen(targetX, targetY); const startX = sprite.x; const startY = sprite.y; let progress = 0; const animate = () => { progress += 0.08; if (progress >= 1) { sprite.x = targetPos.x; sprite.y = targetPos.y; sprite.gridX = targetX; sprite.gridY = targetY; return; } sprite.x = startX + (targetPos.x - startX) * progress; sprite.y = startY + (targetPos.y - startY) * progress; requestAnimationFrame(animate); }; animate(); } initializeRenderer() { this.app = new PIXI.Application({ width: window.innerWidth, height: window.innerHeight, backgroundAlpha: 0, antialias: true, resolution: window.devicePixelRatio || 1, autoDensity: true }); document.getElementById('game').appendChild(this.app.view); const tiles = new PIXI.Container(); this.app.stage.addChild(tiles); this.world.tiles.forEach(tileData => { const tile = createTile(tileData, this.theme); tile.on('pointerdown', () => this.movePetTo(tile.gridX, tile.gridY)); tiles.addChild(tile); }); this.pet = createPet(this.world.pet.startX, this.world.pet.startY, this.playerInfo.name); this.app.stage.addChild(this.pet); this.dialogue = document.getElementById('llm'); this.setupInput(); this.app.ticker.add(() => this.gameLoop()); } movePetTo(targetX, targetY) { if (this.isMoving || targetX < 0 || targetX >= this.world.gridSize || targetY < 0 || targetY >= this.world.gridSize) return; this.isMoving = true; const targetPos = isoToScreen(targetX, targetY); const startX = this.pet.x; const startY = this.pet.y; let progress = 0; const animate = () => { progress += 0.08; if (progress >= 1) { this.pet.x = targetPos.x; this.pet.y = targetPos.y; this.pet.gridX = targetX; this.pet.gridY = targetY; this.isMoving = false; // Use API action to send move 📤 if (this.worldActions) { this.worldActions.move(targetX, targetY); } return; } this.pet.x = startX + (targetPos.x - startX) * progress; this.pet.y = startY + (targetPos.y - startY) * progress; requestAnimationFrame(animate); }; animate(); } setupInput() { window.addEventListener('keydown', (e) => { if (this.dialogue.isOpen) return; this.keys[e.key.toLowerCase()] = true; if (!this.isMoving) { let newX = this.pet.gridX; let newY = this.pet.gridY; if (this.keys['w'] || this.keys['arrowup']) { newY--; } else if (this.keys['s'] || this.keys['arrowdown']) { newY++; } else if (this.keys['a'] || this.keys['arrowleft']) { newX--; } else if (this.keys['d'] || this.keys['arrowright']) { newX++; } this.movePetTo(newX, newY); } }); window.addEventListener('keyup', (e) => { this.keys[e.key.toLowerCase()] = false; }); } gameLoop() { if (!this.isMoving && this.pet) { this.pet.children[0].y = -30 + Math.sin(Date.now() / 300) * 2; } } } // ============================================ // START GAME // ============================================ const urlParams = new URLSearchParams(window.location.search); const worldId = urlParams.get('world'); const game = new Game(); game.worldId = worldId; game.init();