generated from ztimson/template
This commit is contained in:
401
public/world.js
Normal file
401
public/world.js
Normal file
@@ -0,0 +1,401 @@
|
||||
// ============================================
|
||||
// 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();
|
||||
Reference in New Issue
Block a user