Refactored code formatting to use consistent indentation and object destructuring across client and server files.
All checks were successful
Build and publish / Build Container (push) Successful in 1m44s

This commit is contained in:
2026-03-02 12:28:18 -05:00
parent 09a59f170c
commit 7b2621c264
5 changed files with 627 additions and 519 deletions

View File

@@ -13,8 +13,10 @@ function shadeColor(hex, amount) {
}
function hex2Int(hex) {
let r = 0, g = 0, b = 0;
if (hex.length === 4) {
let r = 0,
g = 0,
b = 0;
if(hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16);
g = parseInt(hex[2] + hex[2], 16);
b = parseInt(hex[3] + hex[3], 16);
@@ -23,15 +25,19 @@ function shadeColor(hex, amount) {
g = parseInt(hex.slice(3, 5), 16);
b = parseInt(hex.slice(5, 7), 16);
}
return { r, g, b };
return {
r,
g,
b
};
}
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1 / 6) return p + (q - p) * 6 * t;
if(t < 1 / 2) return q;
if(t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
@@ -39,21 +45,38 @@ function shadeColor(hex, amount) {
return '#' + dec2Hex(r) + dec2Hex(g) + dec2Hex(b);
}
let { r, g, b } = hex2Int(hex);
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
let {
r,
g,
b
} = hex2Int(hex);
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b),
min = Math.min(r, g, b);
let h,
s,
l = (max + min) / 2;
if (max === min) {
if(max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
default: h = 0; break;
switch(max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
default:
h = 0;
break;
}
h /= 6;
}
@@ -67,7 +90,7 @@ function shadeColor(hex, amount) {
class BtnComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>

View File

@@ -9,216 +9,216 @@ const TILE_DEPTH = 16;
// THEME HANDLER
// ============================================
function applyTheme(theme) {
const body = document.body;
const body = document.body;
if (theme.background.image) {
body.style.backgroundImage = `url(${theme.background.image})`;
}
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';
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);
});
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);
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
};
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 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'))
};
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();
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();
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();
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();
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();
drawNormalTile();
graphics.interactive = true;
graphics.buttonMode = true;
graphics.gridX = tileData.x;
graphics.gridY = tileData.y;
graphics.interactive = true;
graphics.buttonMode = true;
graphics.gridX = tileData.x;
graphics.gridY = tileData.y;
graphics.on('pointerover', () => {
drawHighlightTile();
});
graphics.on('pointerover', () => {
drawHighlightTile();
});
graphics.on('pointerout', () => {
drawNormalTile();
});
graphics.on('pointerout', () => {
drawNormalTile();
});
return graphics;
return graphics;
}
function createPet(gridX, gridY, name = 'PET') {
const container = new PIXI.Container();
const pos = isoToScreen(gridX, gridY);
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();
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(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();
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;
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;
container.addChild(body);
container.addChild(nameText);
container.x = pos.x;
container.y = pos.y;
container.gridX = gridX;
container.gridY = gridY;
return container;
return container;
}
// ============================================
// GAME CLASS
// ============================================
class Game {
constructor() {
this.worldId = '';
this.world = null;
this.app = null;
this.pet = null;
this.otherPlayers = new Map();
this.isMoving = false;
this.dialogue = null;
this.keys = {};
constructor() {
this.worldId = '';
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.navi = window.navi;
this.worldActions = null;
// Use global singleton 🌍
this.navi = window.navi;
this.worldActions = null;
this.playerInfo = {
name: 'Guest',
apiUrl: this.navi.navi
};
}
this.playerInfo = {
name: 'Guest',
apiUrl: this.navi.navi
};
}
async init() {
try {
// Join world with callbacks 🌍
async init() {
try {
// Join world with callbacks 🌍
await this.navi.init();
this.worldActions = this.navi.connect(this.worldId);
this.worldActions = this.navi.connect(this.worldId);
this.worldActions.onData = (data) => {
this.world = data;
applyTheme(this.world.theme);
this.initializeRenderer();
}
};
this.worldActions.onPlayers = (players) => {
players.forEach(player => {
if (player.name !== this.navi.info.name) {
if(player.name !== this.navi.info.name) {
this.addOtherPlayer(player);
}
});
}
};
this.worldActions.onJoined = (player) => {
this.addOtherPlayer(player);
}
};
this.worldActions.onMoved = (data) => {
const sprite = this.otherPlayers.get(data.socketId);
if (sprite) {
if(sprite) {
this.moveOtherPlayer(sprite, data.x, data.y);
}
};
@@ -226,149 +226,147 @@ class Game {
this.worldActions.onLeft = (data) => {
const sprite = this.otherPlayers.get(data.socketId);
if(sprite) this.otherPlayers.delete(data.socketId);
}
};
this.worldActions.onError = (error) => console.error('❌ World error:', error);
console.log('✨ Game initializing...');
} catch (error) {
console.error('❌ Failed to initialize game:', 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);
}
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);
moveOtherPlayer(sprite, targetX, targetY) {
const targetPos = isoToScreen(targetX, targetY);
const startX = sprite.x;
const startY = sprite.y;
let progress = 0;
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;
}
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;
sprite.x = startX + (targetPos.x - startX) * progress;
sprite.y = startY + (targetPos.y - startY) * progress;
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
};
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);
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);
const tiles = new PIXI.Container();
this.app.stage.addChild(tiles);
this.world.tiles.forEach(tileData => {
const tile = createTile(tileData, this.world.theme);
tile.on('pointerdown', () => this.movePetTo(tile.gridX, tile.gridY));
tiles.addChild(tile);
});
this.world.tiles.forEach(tileData => {
const tile = createTile(tileData, this.world.theme);
tile.on('pointerdown', () => this.movePetTo(tile.gridX, tile.gridY));
tiles.addChild(tile);
});
const spawn = this.world.tiles.find(t => t.type === 'spawn');
this.pet = createPet(spawn.x, spawn.y, this.playerInfo.name);
this.app.stage.addChild(this.pet);
this.pet = createPet(spawn.x, spawn.y, this.playerInfo.name);
this.app.stage.addChild(this.pet);
this.dialogue = document.getElementById('llm');
this.dialogue = document.getElementById('llm');
this.setupInput();
this.app.ticker.add(() => this.gameLoop());
}
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;
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);
this.isMoving = true;
const targetPos = isoToScreen(targetX, targetY);
const startX = this.pet.x;
const startY = this.pet.y;
let progress = 0;
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;
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;
}
// 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;
this.pet.x = startX + (targetPos.x - startX) * progress;
this.pet.y = startY + (targetPos.y - startY) * progress;
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
};
animate();
}
animate();
}
setupInput() {
window.addEventListener('keydown', (e) => {
if (this.dialogue.isOpen) return;
setupInput() {
window.addEventListener('keydown', (e) => {
if(this.dialogue.isOpen) return;
this.keys[e.key.toLowerCase()] = true;
this.keys[e.key.toLowerCase()] = true;
if (!this.isMoving) {
let newX = this.pet.gridX;
let newY = this.pet.gridY;
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++;
}
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);
}
});
this.movePetTo(newX, newY);
}
});
window.addEventListener('keyup', (e) => {
this.keys[e.key.toLowerCase()] = false;
});
}
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;
}
}
gameLoop() {
if(!this.isMoving && this.pet) {
this.pet.children[0].y = -30 + Math.sin(Date.now() / 300) * 2;
}
}
}
// ============================================

View File

@@ -2,10 +2,10 @@
<html>
<head>
<title>NetNavi v1.0.0</title>
<link rel="icon" href="/favicon.png" />
<link rel="icon" href="/favicon.png"/>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/7.3.2/pixi.min.js"></script>
@@ -50,6 +50,7 @@
<script type="module">
import Navi from './navi.mjs';
window.navi = new Navi();
</script>
<script type="module" src="/components/jukebox.mjs"></script>

View File

@@ -1,4 +1,4 @@
export default class Navi {
class Navi {
api;
connected = false;
icon;
@@ -12,11 +12,11 @@ export default class Navi {
#secret;
#world;
constructor(api = window.location.origin, secret = '') {
this.api = api.replace(/\/$/, '');
constructor(api = window.location.origin, secret = '') {
this.api = api.replace(/\/$/, '');
this.icon = `${this.api}/favicon.png`;
this.#secret = secret;
}
}
async init() {
if(this.#init) return this.#init;
@@ -56,7 +56,7 @@ export default class Navi {
this.connected = true;
this.emit('init');
res(this);
} catch (err) {
} catch(err) {
rej(err);
}
});
@@ -86,41 +86,49 @@ export default class Navi {
callbacks.forEach(cb => cb(data));
}
// ============================================
// REST API
// ============================================
// ============================================
// REST API
// ============================================
async sendUserMessage(userId, message) {
const response = await fetch(`${this.api}/api/message`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
if (!response.ok) throw new Error('Message failed to send bestie');
async sendUserMessage(userId, message) {
const response = await fetch(`${this.api}/api/message`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message})
});
if(!response.ok) throw new Error('Message failed to send bestie');
const result = await response.json();
this.emit('message:sent', { userId, message, result });
return result;
}
const result = await response.json();
this.emit('message:sent', {
userId,
message,
result
});
return result;
}
async linkPet(petId, targetApiUrl) {
const response = await fetch(`${this.api}/api/link`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetApiUrl })
});
if (!response.ok) throw new Error('Link connection is bussin (negatively)');
async linkPet(petId, targetApiUrl) {
const response = await fetch(`${this.api}/api/link`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({targetApiUrl})
});
if(!response.ok) throw new Error('Link connection is bussin (negatively)');
const result = await response.json();
this.emit('pet:linked', { petId, targetApiUrl, result });
return result;
}
const result = await response.json();
this.emit('pet:linked', {
petId,
targetApiUrl,
result
});
return result;
}
// ============================================
// WORLD SOCKET
// ============================================
// ============================================
// WORLD SOCKET
// ============================================
connect(apiOrWorld, world) {
connect(apiOrWorld, world) {
let api;
if(world) {
api = apiOrWorld;
@@ -129,12 +137,12 @@ export default class Navi {
world = apiOrWorld;
}
if(!this.world) this.world = {};
if(this.#world && this.world.api !== api) {
if(!this.world) this.world = {};
if(this.#world && this.world.api !== api) {
this.#world.disconnect();
this.#world = null;
}
if(!this.#world) this.#world = io(`${api}/world`, {auth: this.#secret ? {token: this.#secret} : null});
}
if(!this.#world) this.#world = io(`${api}/world`, {auth: this.#secret ? {token: this.#secret} : null});
this.world.api = api;
this.world.data = null;
this.world.name = world;
@@ -142,113 +150,127 @@ export default class Navi {
const callbacks = {
move: (x, y) => {
this.#world.emit('move', {x, y});
this.#world.emit('move', {
x,
y
});
},
leave: () => {
this.#world.disconnect();
this.world = null;
},
onData: (data) => { },
onPlayers: (players) => { },
onJoined: (player) => { },
onMoved: (player) => { },
onLeft: (player) => { },
onError: (error) => { }
}
onData: (data) => {
},
onPlayers: (players) => {
},
onJoined: (player) => {
},
onMoved: (player) => {
},
onLeft: (player) => {
},
onError: (error) => {
}
};
this.#world.on('data', (data) => {
this.#world.on('data', (data) => {
this.world.data = data;
callbacks.onData(data);
callbacks.onData(data);
this.emit('world:data', data);
});
});
this.#world.on('players', (players) => {
this.world.players.clear();
players.forEach(p => this.world.players.set(p.socketId, p));
callbacks.onPlayers(players);
this.emit('world:players', players)
});
this.#world.on('players', (players) => {
this.world.players.clear();
players.forEach(p => this.world.players.set(p.socketId, p));
callbacks.onPlayers(players);
this.emit('world:players', players);
});
this.#world.on('joined', (player) => {
this.world.players.set(player.socketId, player);
callbacks.onJoined(player);
this.#world.on('joined', (player) => {
this.world.players.set(player.socketId, player);
callbacks.onJoined(player);
this.emit('world:joined', player);
});
});
this.#world.on('moved', (data) => {
const player = this.world.players.get(data.socketId);
if(player) {
player.x = data.x;
player.y = data.y;
}
callbacks.onMoved(player);
this.emit('world:moved', player);
});
this.#world.on('left', (data) => {
this.#world.on('moved', (data) => {
const player = this.world.players.get(data.socketId);
this.world.players.delete(data.socketId);
callbacks.onLeft(player);
if(player) {
player.x = data.x;
player.y = data.y;
}
callbacks.onMoved(player);
this.emit('world:moved', player);
});
this.#world.on('left', (data) => {
const player = this.world.players.get(data.socketId);
this.world.players.delete(data.socketId);
callbacks.onLeft(player);
this.emit('world:left', player);
});
});
this.#world.on('error', (error) => {
console.error('World error:', error);
callbacks.onError(error);
this.#world.on('error', (error) => {
console.error('World error:', error);
callbacks.onError(error);
this.emit('world:error', error);
});
});
this.#world.emit('join', {world, api});
this.#world.emit('join', {
world,
api
});
this.emit('world:join', world);
return callbacks;
}
return callbacks;
}
// ============================================
// LLM
// ============================================
// ============================================
// LLM
// ============================================
ask(message, stream) {
this.llmCallback = stream;
const promise = new Promise((resolve, reject) => {
this.llmResolve = resolve;
this.llmReject = reject;
this.#socket.emit('llm-ask', {message});
this.emit('llm:ask')
});
ask(message, stream) {
this.llmCallback = stream;
const promise = new Promise((resolve, reject) => {
this.llmResolve = resolve;
this.llmReject = reject;
this.#socket.emit('llm-ask', {message});
this.emit('llm:ask');
});
promise.abort = () => {
this.#socket.emit('llm-abort');
if(this.llmReject) this.llmReject(new Error('Aborted by user'));
this.llmCallback = null;
this.llmResolve = null;
this.llmReject = null;
this.emit('llm:abort')
};
promise.abort = () => {
this.#socket.emit('llm-abort');
if(this.llmReject) this.llmReject(new Error('Aborted by user'));
this.llmCallback = null;
this.llmResolve = null;
this.llmReject = null;
this.emit('llm:abort');
};
return promise;
}
return promise;
}
clearChat() {
if(this.#socket) this.#socket.emit('llm-clear');
clearChat() {
if(this.#socket) this.#socket.emit('llm-clear');
this.emit('llm:clear');
}
}
// ============================================
// UTILITY
// ============================================
// ============================================
// UTILITY
// ============================================
disconnect() {
this.connected = false;
disconnect() {
this.connected = false;
this.icon = this.info = this.theme = this.world = this.#init = this.#secret = null;
if(this.#world) {
this.#world.disconnect();
this.#world = null;
}
if(this.#socket) {
this.#socket.disconnect();
this.#socket = null;
}
if(this.#world) {
this.#world.disconnect();
this.#world = null;
}
if(this.#socket) {
this.#socket.disconnect();
this.#socket = null;
}
this.emit('disconnected');
}
}
}
export default Navi;

View File

@@ -1,12 +1,34 @@
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
import cors from 'cors';
import fs from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import {Ai, DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool} from '@ztimson/ai-utils';
import {contrast, shadeColor} from '@ztimson/utils';
import express
from 'express';
import {
createServer
} from 'http';
import {
Server
} from 'socket.io';
import cors
from 'cors';
import fs
from 'fs';
import {
join,
dirname
} from 'path';
import {
fileURLToPath
} from 'url';
import {
Ai,
DateTimeTool,
ExecTool,
FetchTool,
ReadWebpageTool,
WebSearchTool
} from '@ztimson/ai-utils';
import {
contrast,
shadeColor
} from '@ztimson/utils';
// ============================================
// Settings
@@ -38,67 +60,77 @@ function calcColors(theme) {
mutedContrast: contrast(theme.muted),
mutedDark: shadeColor(theme.muted, -.1),
mutedLight: shadeColor(theme.muted, .1),
}
};
}
let memories = [], settings = {
name: 'Navi',
personality: 'You are inquisitive about your user trying to best adjust your personally to fit them',
instructions: '',
theme: {
background: '#fff',
border: '#000',
text: '#252525',
primary: '#9f32ef',
accent: '#6f16c3',
muted: '#a8a8a8',
}
};
let memories = [],
settings = {
name: 'Navi',
personality: 'You are inquisitive about your user trying to best adjust your personally to fit them',
instructions: '',
theme: {
background: '#fff',
border: '#000',
text: '#252525',
primary: '#9f32ef',
accent: '#6f16c3',
muted: '#a8a8a8',
}
};
function load() {
try {
try {
settings = {
...settings,
...JSON.parse(fs.readFileSync(settingsFile, 'utf-8'))
};
} catch { }
try {
memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8'));
} catch { }
} catch { }
try {
memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8'));
} catch { }
}
function save() {
if(!updated) return;
updated = false;
const dir = dirname(settingsFile);
if (!fs.existsSync(dir)) {
fs.mkdir(dir, { recursive: true }, (err) => {
if (err) throw err; // Fail loudly if dirs cant be made 💀
});
}
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
fs.writeFileSync(memoriesFile, JSON.stringify(memories, null, 2));
const dir = dirname(settingsFile);
if(!fs.existsSync(dir)) {
fs.mkdir(dir, {recursive: true}, (err) => {
if(err) throw err; // Fail loudly if dirs cant be made 💀
});
}
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
fs.writeFileSync(memoriesFile, JSON.stringify(memories, null, 2));
}
load();
const ai = new Ai({
llm: {
models: {
'Ministral-3': {proto: 'openai', host: 'http://10.69.0.55:11728', token: 'ignore'},
},
system: `You are a NetNavi, personal assistant & companion. Keep responses short and unstyled. You have your own debian environment you can access using the exec tool with the cli language. Use your remember tool liberally to store all facts. Adjust your personality with tools based on your interactions with the user.\n\nPersonality:\n${settings.personality || ''}\n\nUser Requests:\n${settings.instructions || ''}`,
tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, {
name: 'adjust_personality',
description: 'Replace your current personality instructions',
args: {instructions: {type: 'string', description: 'Bullet point list of how to behave'}},
fn: (args) => {
settings.personality = args.instructions;
updated = true;
return 'done!';
}
}],
},
llm: {
models: {
'Ministral-3': {
proto: 'openai',
host: 'http://10.69.0.55:11728',
token: 'ignore'
},
},
system: `You are a NetNavi, personal assistant & companion. Keep responses short and unstyled. You have your own debian environment you can access using the exec tool with the cli language, use /tmp as your working directory. Use your remember tool liberally to store all facts. Adjust your personality with tools based on your interactions with the user.\n\nPersonality:\n${settings.personality || ''}\n\nUser Requests:\n${settings.instructions || ''}`,
tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, {
name: 'adjust_personality',
description: 'Replace your current personality instructions',
args: {
instructions: {
type: 'string',
description: 'Bullet point list of how to behave'
}
},
fn: (args) => {
settings.personality = args.instructions;
updated = true;
return 'done!';
}
}],
},
});
// ============================================
@@ -108,7 +140,10 @@ const ai = new Ai({
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {origin: "*", methods: ["GET", "POST"]}
cors: {
origin: '*',
methods: ['GET', 'POST']
}
});
app.use(cors());
@@ -123,42 +158,42 @@ const chatHistory = new Map();
io.on('connection', (socket) => {
console.debug('👤 User connected:', socket.id);
chatHistory.set(socket.id, []);
let currentRequest = null;
chatHistory.set(socket.id, []);
let currentRequest = null;
socket.on('llm-clear', async () => {
chatHistory.set(socket.id, []);
});
socket.on('llm-clear', async () => {
chatHistory.set(socket.id, []);
});
socket.on('llm-abort', () => {
if (currentRequest?.abort) {
currentRequest.abort();
currentRequest = null;
}
});
socket.on('llm-abort', () => {
if(currentRequest?.abort) {
currentRequest.abort();
currentRequest = null;
}
});
socket.on('llm-ask', async (data) => {
const { message } = data;
const history = chatHistory.get(socket.id);
currentRequest = ai.language.ask(message, {
history,
memory: memories,
stream: (chunk) => socket.emit('llm-stream', chunk)
}).then(resp => {
chatHistory.set(socket.id, history);
socket.emit('llm-response', { message: resp });
socket.on('llm-ask', async (data) => {
const {message} = data;
const history = chatHistory.get(socket.id);
currentRequest = ai.language.ask(message, {
history,
memory: memories,
stream: (chunk) => socket.emit('llm-stream', chunk)
}).then(resp => {
chatHistory.set(socket.id, history);
socket.emit('llm-response', {message: resp});
updated = true;
}).catch(err => {
socket.emit('llm-error', {message: err.message || err.toString()});
}).finally(() => {
currentRequest = null;
});
});
}).catch(err => {
socket.emit('llm-error', {message: err.message || err.toString()});
}).finally(() => {
currentRequest = null;
});
});
socket.on('disconnect', () => {
console.log('👤 User disconnected:', socket.id);
chatHistory.delete(socket.id);
});
socket.on('disconnect', () => {
console.log('👤 User disconnected:', socket.id);
chatHistory.delete(socket.id);
});
});
// ============================================
@@ -168,22 +203,26 @@ const worldPlayers = new Map();
// Load world data
function loadWorld(name) {
let w, t;
let w,
t;
try {
w = JSON.parse(fs.readFileSync(join(worlds, (name || 'home') + '.json'), 'utf-8'));
} catch (error) {
} catch(error) {
console.error(`Failed to load world ${name}:`, error);
return null;
}
try {
t = JSON.parse(fs.readFileSync(join(protocols, w.theme + '.json'), 'utf-8'));
t.colors = calcColors(t.colors);
} catch (error) {
} catch(error) {
console.error(`Failed to load theme protocol ${w.theme}:`, error);
return null;
}
worldPlayers.set(name, new Map());
return {...w, theme: t};
return {
...w,
theme: t
};
}
io.of('/world').on('connection', (socket) => {
@@ -194,7 +233,10 @@ io.of('/world').on('connection', (socket) => {
// Join a world
socket.on('join', async (data) => {
const { world, api } = data;
const {
world,
api
} = data;
const info = await fetch(api.replace(/\/$/, '') + '/api/info').then(resp => {
if(resp.ok) return resp.json();
socket.emit('error', {message: `Invalid Navi API: ${api}`});
@@ -202,7 +244,7 @@ io.of('/world').on('connection', (socket) => {
});
const worldData = loadWorld(world);
if(!worldData) return socket.emit('error', { message: 'World not found' });
if(!worldData) return socket.emit('error', {message: 'World not found'});
// Leave previous world if any
if(currentWorld) {
@@ -236,10 +278,17 @@ io.of('/world').on('connection', (socket) => {
// Player movement
socket.on('move', (data) => {
if(!currentWorld || !playerData) return;
const { x, y } = data;
const {
x,
y
} = data;
playerData.x = x;
playerData.y = y;
socket.to(`world:${currentWorld}`).emit('moved', {socketId: socket.id, x, y});
socket.to(`world:${currentWorld}`).emit('moved', {
socketId: socket.id,
x,
y
});
});
// Disconnect
@@ -260,12 +309,12 @@ io.of('/world').on('connection', (socket) => {
// ============================================
app.get('/favicon.*', (req, res) => {
res.sendFile(logoFile);
res.sendFile(logoFile);
});
// Get Navi info
app.get('/api/info', (req, res) => {
res.json({
res.json({
name: settings.name,
theme: calcColors(settings.theme),
});
@@ -273,41 +322,56 @@ app.get('/api/info', (req, res) => {
// Get sprite sheet
app.get('/api/sprite', (req, res) => {
const { petId } = req.params;
// TODO: Return actual sprite sheet URL
res.json({
spriteUrl: '/sprites/default-pet.png',
frameWidth: 32,
frameHeight: 32,
animations: {
idle: { frames: [0, 1, 2, 3], speed: 200 },
walk: { frames: [4, 5, 6, 7], speed: 100 }
}
});
const {petId} = req.params;
// TODO: Return actual sprite sheet URL
res.json({
spriteUrl: '/sprites/default-pet.png',
frameWidth: 32,
frameHeight: 32,
animations: {
idle: {
frames: [0, 1, 2, 3],
speed: 200
},
walk: {
frames: [4, 5, 6, 7],
speed: 100
}
}
});
});
// Send message to user (push notification / email / etc)
app.post('/api/message', (req, res) => {
const { userId } = req.params;
const { message } = req.body;
// TODO: Implement notification system
res.json({ success: true, message: 'Message sent' });
const {userId} = req.params;
const {message} = req.body;
// TODO: Implement notification system
res.json({
success: true,
message: 'Message sent'
});
});
// Send message to PET LLM
app.post('/api/message', (req, res) => {
const { petId } = req.params;
const { message } = req.body;
// TODO: Queue message for LLM processing
res.json({ success: true, message: 'Message queued' });
const {petId} = req.params;
const {message} = req.body;
// TODO: Queue message for LLM processing
res.json({
success: true,
message: 'Message queued'
});
});
// Link another PET
app.post('/api/link', (req, res) => {
const { petId } = req.params;
const { targetApiUrl } = req.body;
// TODO: Store link in database
res.json({success: true, message: 'PET linked'});
const {petId} = req.params;
const {targetApiUrl} = req.body;
// TODO: Store link in database
res.json({
success: true,
message: 'PET linked'
});
});
// ============================================
@@ -317,7 +381,7 @@ const PORT = process.env.PORT || 3000;
setInterval(() => save(), 5 * 60_000);
httpServer.listen(PORT, () => {
console.log(`🚀 Server running on: http://localhost:${PORT}`);
console.log(`🚀 Server running on: http://localhost:${PORT}`);
});
function shutdown() {