generated from ztimson/template
All checks were successful
Build and publish / Build Container (push) Successful in 1m27s
398 lines
12 KiB
JavaScript
398 lines
12 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.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();
|