Files
navi/public/world.js
ztimson 4735968612
All checks were successful
Build and publish / Build Container (push) Successful in 1m29s
Nav init
2026-02-28 23:27:35 -05:00

402 lines
13 KiB
JavaScript

// ============================================
// 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.audioManager = 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) => {
console.log('👥 Current players:', players);
players.forEach(player => {
if (player.socketId !== this.api.worldSocket.id) {
this.addOtherPlayer(player);
}
});
},
onPlayerJoined: (player) => {
console.log('👋 Player joined:', player.name);
this.addOtherPlayer(player);
},
onPlayerMoved: (data) => {
const sprite = this.otherPlayers.get(data.socketId);
if (sprite) {
this.moveOtherPlayer(sprite, data.x, data.y);
}
},
onPlayerLeft: (data) => {
console.log('👋 Player left:', data.socketId);
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.body.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();