generated from ztimson/template
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
All checks were successful
Build and publish / Build Container (push) Successful in 1m44s
This commit is contained in:
@@ -13,8 +13,10 @@ function shadeColor(hex, amount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hex2Int(hex) {
|
function hex2Int(hex) {
|
||||||
let r = 0, g = 0, b = 0;
|
let r = 0,
|
||||||
if (hex.length === 4) {
|
g = 0,
|
||||||
|
b = 0;
|
||||||
|
if(hex.length === 4) {
|
||||||
r = parseInt(hex[1] + hex[1], 16);
|
r = parseInt(hex[1] + hex[1], 16);
|
||||||
g = parseInt(hex[2] + hex[2], 16);
|
g = parseInt(hex[2] + hex[2], 16);
|
||||||
b = parseInt(hex[3] + hex[3], 16);
|
b = parseInt(hex[3] + hex[3], 16);
|
||||||
@@ -23,15 +25,19 @@ function shadeColor(hex, amount) {
|
|||||||
g = parseInt(hex.slice(3, 5), 16);
|
g = parseInt(hex.slice(3, 5), 16);
|
||||||
b = parseInt(hex.slice(5, 7), 16);
|
b = parseInt(hex.slice(5, 7), 16);
|
||||||
}
|
}
|
||||||
return { r, g, b };
|
return {
|
||||||
|
r,
|
||||||
|
g,
|
||||||
|
b
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function hue2rgb(p, q, t) {
|
function hue2rgb(p, q, t) {
|
||||||
if (t < 0) t += 1;
|
if(t < 0) t += 1;
|
||||||
if (t > 1) t -= 1;
|
if(t > 1) t -= 1;
|
||||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
if(t < 1 / 6) return p + (q - p) * 6 * t;
|
||||||
if (t < 1 / 2) return q;
|
if(t < 1 / 2) return q;
|
||||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
if(t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,21 +45,38 @@ function shadeColor(hex, amount) {
|
|||||||
return '#' + dec2Hex(r) + dec2Hex(g) + dec2Hex(b);
|
return '#' + dec2Hex(r) + dec2Hex(g) + dec2Hex(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
let { r, g, b } = hex2Int(hex);
|
let {
|
||||||
r /= 255; g /= 255; b /= 255;
|
r,
|
||||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
g,
|
||||||
let h, s, l = (max + min) / 2;
|
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;
|
h = s = 0;
|
||||||
} else {
|
} else {
|
||||||
const d = max - min;
|
const d = max - min;
|
||||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
switch (max) {
|
switch(max) {
|
||||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
case r:
|
||||||
case g: h = (b - r) / d + 2; break;
|
h = (g - b) / d + (g < b ? 6 : 0);
|
||||||
case b: h = (r - g) / d + 4; break;
|
break;
|
||||||
default: h = 0; break;
|
case g:
|
||||||
|
h = (b - r) / d + 2;
|
||||||
|
break;
|
||||||
|
case b:
|
||||||
|
h = (r - g) / d + 4;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
h = 0;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
h /= 6;
|
h /= 6;
|
||||||
}
|
}
|
||||||
@@ -67,7 +90,7 @@ function shadeColor(hex, amount) {
|
|||||||
class BtnComponent extends HTMLElement {
|
class BtnComponent extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({mode: 'open'});
|
||||||
|
|
||||||
this.shadowRoot.innerHTML = `
|
this.shadowRoot.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -9,216 +9,216 @@ const TILE_DEPTH = 16;
|
|||||||
// THEME HANDLER
|
// THEME HANDLER
|
||||||
// ============================================
|
// ============================================
|
||||||
function applyTheme(theme) {
|
function applyTheme(theme) {
|
||||||
const body = document.body;
|
const body = document.body;
|
||||||
|
|
||||||
if (theme.background.image) {
|
if(theme.background.image) {
|
||||||
body.style.backgroundImage = `url(${theme.background.image})`;
|
body.style.backgroundImage = `url(${theme.background.image})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.style.backgroundSize = theme.background.style || 'cover';
|
body.style.backgroundSize = theme.background.style || 'cover';
|
||||||
body.style.backgroundPosition = 'center';
|
body.style.backgroundPosition = 'center';
|
||||||
body.style.backgroundRepeat = 'no-repeat';
|
body.style.backgroundRepeat = 'no-repeat';
|
||||||
body.style.backgroundAttachment = 'fixed';
|
body.style.backgroundAttachment = 'fixed';
|
||||||
|
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
Object.entries(theme.colors).forEach(([key, value]) => {
|
Object.entries(theme.colors).forEach(([key, value]) => {
|
||||||
const cssVar = `--${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
|
const cssVar = `--${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
|
||||||
root.style.setProperty(cssVar, value);
|
root.style.setProperty(cssVar, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('🎨 Theme applied:', theme.name);
|
console.log('🎨 Theme applied:', theme.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// TILE RENDERER
|
// TILE RENDERER
|
||||||
// ============================================
|
// ============================================
|
||||||
function isoToScreen(gridX, gridY) {
|
function isoToScreen(gridX, gridY) {
|
||||||
return {
|
return {
|
||||||
x: (gridX - gridY) * (TILE_WIDTH / 2) + window.innerWidth / 2,
|
x: (gridX - gridY) * (TILE_WIDTH / 2) + window.innerWidth / 2,
|
||||||
y: (gridX + gridY) * (TILE_HEIGHT / 2) + 100
|
y: (gridX + gridY) * (TILE_HEIGHT / 2) + 100
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTile(tileData, theme) {
|
function createTile(tileData, theme) {
|
||||||
const graphics = new PIXI.Graphics();
|
const graphics = new PIXI.Graphics();
|
||||||
const pos = isoToScreen(tileData.x, tileData.y);
|
const pos = isoToScreen(tileData.x, tileData.y);
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
top: parseInt(theme.colors.tileTop.replace('#', '0x')),
|
top: parseInt(theme.colors.tileTop.replace('#', '0x')),
|
||||||
side: parseInt(theme.colors.tileSide.replace('#', '0x')),
|
side: parseInt(theme.colors.tileSide.replace('#', '0x')),
|
||||||
grid: parseInt(theme.colors.gridColor.replace('#', '0x')),
|
grid: parseInt(theme.colors.gridColor.replace('#', '0x')),
|
||||||
highlight: parseInt(theme.colors.tileHighlight.replace('#', '0x')),
|
highlight: parseInt(theme.colors.tileHighlight.replace('#', '0x')),
|
||||||
gridHighlight: parseInt(theme.colors.gridHighlight.replace('#', '0x'))
|
gridHighlight: parseInt(theme.colors.gridHighlight.replace('#', '0x'))
|
||||||
};
|
};
|
||||||
|
|
||||||
function drawNormalTile() {
|
function drawNormalTile() {
|
||||||
graphics.clear();
|
graphics.clear();
|
||||||
graphics.beginFill(colors.top);
|
graphics.beginFill(colors.top);
|
||||||
graphics.lineStyle(1, colors.grid);
|
graphics.lineStyle(1, colors.grid);
|
||||||
graphics.moveTo(pos.x, pos.y);
|
graphics.moveTo(pos.x, pos.y);
|
||||||
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
graphics.lineTo(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);
|
||||||
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
graphics.lineTo(pos.x, pos.y);
|
graphics.lineTo(pos.x, pos.y);
|
||||||
graphics.endFill();
|
graphics.endFill();
|
||||||
|
|
||||||
graphics.beginFill(colors.side);
|
graphics.beginFill(colors.side);
|
||||||
graphics.lineStyle(1, colors.grid);
|
graphics.lineStyle(1, colors.grid);
|
||||||
graphics.moveTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
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);
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
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 + TILE_DEPTH);
|
||||||
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
graphics.endFill();
|
graphics.endFill();
|
||||||
|
|
||||||
graphics.beginFill(colors.side);
|
graphics.beginFill(colors.side);
|
||||||
graphics.lineStyle(1, colors.grid);
|
graphics.lineStyle(1, colors.grid);
|
||||||
graphics.moveTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
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);
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
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 + TILE_DEPTH);
|
||||||
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
graphics.endFill();
|
graphics.endFill();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawHighlightTile() {
|
function drawHighlightTile() {
|
||||||
graphics.clear();
|
graphics.clear();
|
||||||
graphics.beginFill(colors.highlight);
|
graphics.beginFill(colors.highlight);
|
||||||
graphics.lineStyle(2, colors.gridHighlight);
|
graphics.lineStyle(2, colors.gridHighlight);
|
||||||
graphics.moveTo(pos.x, pos.y);
|
graphics.moveTo(pos.x, pos.y);
|
||||||
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
graphics.lineTo(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);
|
||||||
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
graphics.lineTo(pos.x, pos.y);
|
graphics.lineTo(pos.x, pos.y);
|
||||||
graphics.endFill();
|
graphics.endFill();
|
||||||
|
|
||||||
graphics.beginFill(colors.side);
|
graphics.beginFill(colors.side);
|
||||||
graphics.lineStyle(1, colors.gridHighlight);
|
graphics.lineStyle(1, colors.gridHighlight);
|
||||||
graphics.moveTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
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);
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
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 + TILE_DEPTH);
|
||||||
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
graphics.lineTo(pos.x - TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
graphics.endFill();
|
graphics.endFill();
|
||||||
|
|
||||||
graphics.beginFill(colors.side);
|
graphics.beginFill(colors.side);
|
||||||
graphics.lineStyle(1, colors.gridHighlight);
|
graphics.lineStyle(1, colors.gridHighlight);
|
||||||
graphics.moveTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
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);
|
||||||
graphics.lineTo(pos.x, pos.y + TILE_HEIGHT + TILE_DEPTH);
|
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 + TILE_DEPTH);
|
||||||
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
graphics.lineTo(pos.x + TILE_WIDTH / 2, pos.y + TILE_HEIGHT / 2);
|
||||||
graphics.endFill();
|
graphics.endFill();
|
||||||
}
|
}
|
||||||
|
|
||||||
drawNormalTile();
|
drawNormalTile();
|
||||||
|
|
||||||
graphics.interactive = true;
|
graphics.interactive = true;
|
||||||
graphics.buttonMode = true;
|
graphics.buttonMode = true;
|
||||||
graphics.gridX = tileData.x;
|
graphics.gridX = tileData.x;
|
||||||
graphics.gridY = tileData.y;
|
graphics.gridY = tileData.y;
|
||||||
|
|
||||||
graphics.on('pointerover', () => {
|
graphics.on('pointerover', () => {
|
||||||
drawHighlightTile();
|
drawHighlightTile();
|
||||||
});
|
});
|
||||||
|
|
||||||
graphics.on('pointerout', () => {
|
graphics.on('pointerout', () => {
|
||||||
drawNormalTile();
|
drawNormalTile();
|
||||||
});
|
});
|
||||||
|
|
||||||
return graphics;
|
return graphics;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPet(gridX, gridY, name = 'PET') {
|
function createPet(gridX, gridY, name = 'PET') {
|
||||||
const container = new PIXI.Container();
|
const container = new PIXI.Container();
|
||||||
const pos = isoToScreen(gridX, gridY);
|
const pos = isoToScreen(gridX, gridY);
|
||||||
|
|
||||||
const body = new PIXI.Graphics();
|
const body = new PIXI.Graphics();
|
||||||
body.beginFill(0xff6b9d);
|
body.beginFill(0xff6b9d);
|
||||||
body.drawCircle(0, -30, 15);
|
body.drawCircle(0, -30, 15);
|
||||||
body.endFill();
|
body.endFill();
|
||||||
|
|
||||||
body.beginFill(0xffffff);
|
body.beginFill(0xffffff);
|
||||||
body.drawCircle(-5, -32, 4);
|
body.drawCircle(-5, -32, 4);
|
||||||
body.drawCircle(5, -32, 4);
|
body.drawCircle(5, -32, 4);
|
||||||
body.endFill();
|
body.endFill();
|
||||||
|
|
||||||
body.beginFill(0x000000);
|
body.beginFill(0x000000);
|
||||||
body.drawCircle(-5, -32, 2);
|
body.drawCircle(-5, -32, 2);
|
||||||
body.drawCircle(5, -32, 2);
|
body.drawCircle(5, -32, 2);
|
||||||
body.endFill();
|
body.endFill();
|
||||||
|
|
||||||
const nameText = new PIXI.Text(name, {
|
const nameText = new PIXI.Text(name, {
|
||||||
fontFamily: 'Courier New',
|
fontFamily: 'Courier New',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fill: '#ffffff',
|
fill: '#ffffff',
|
||||||
stroke: '#000000',
|
stroke: '#000000',
|
||||||
strokeThickness: 2
|
strokeThickness: 2
|
||||||
});
|
});
|
||||||
nameText.anchor.set(0.5);
|
nameText.anchor.set(0.5);
|
||||||
nameText.y = -50;
|
nameText.y = -50;
|
||||||
|
|
||||||
container.addChild(body);
|
container.addChild(body);
|
||||||
container.addChild(nameText);
|
container.addChild(nameText);
|
||||||
container.x = pos.x;
|
container.x = pos.x;
|
||||||
container.y = pos.y;
|
container.y = pos.y;
|
||||||
container.gridX = gridX;
|
container.gridX = gridX;
|
||||||
container.gridY = gridY;
|
container.gridY = gridY;
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// GAME CLASS
|
// GAME CLASS
|
||||||
// ============================================
|
// ============================================
|
||||||
class Game {
|
class Game {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.worldId = '';
|
this.worldId = '';
|
||||||
this.world = null;
|
this.world = null;
|
||||||
this.app = null;
|
this.app = null;
|
||||||
this.pet = null;
|
this.pet = null;
|
||||||
this.otherPlayers = new Map();
|
this.otherPlayers = new Map();
|
||||||
this.isMoving = false;
|
this.isMoving = false;
|
||||||
this.dialogue = null;
|
this.dialogue = null;
|
||||||
this.keys = {};
|
this.keys = {};
|
||||||
|
|
||||||
// Use global singleton 🌍
|
// Use global singleton 🌍
|
||||||
this.navi = window.navi;
|
this.navi = window.navi;
|
||||||
this.worldActions = null;
|
this.worldActions = null;
|
||||||
|
|
||||||
this.playerInfo = {
|
this.playerInfo = {
|
||||||
name: 'Guest',
|
name: 'Guest',
|
||||||
apiUrl: this.navi.navi
|
apiUrl: this.navi.navi
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
// Join world with callbacks 🌍
|
// Join world with callbacks 🌍
|
||||||
await this.navi.init();
|
await this.navi.init();
|
||||||
this.worldActions = this.navi.connect(this.worldId);
|
this.worldActions = this.navi.connect(this.worldId);
|
||||||
|
|
||||||
this.worldActions.onData = (data) => {
|
this.worldActions.onData = (data) => {
|
||||||
this.world = data;
|
this.world = data;
|
||||||
applyTheme(this.world.theme);
|
applyTheme(this.world.theme);
|
||||||
this.initializeRenderer();
|
this.initializeRenderer();
|
||||||
}
|
};
|
||||||
|
|
||||||
this.worldActions.onPlayers = (players) => {
|
this.worldActions.onPlayers = (players) => {
|
||||||
players.forEach(player => {
|
players.forEach(player => {
|
||||||
if (player.name !== this.navi.info.name) {
|
if(player.name !== this.navi.info.name) {
|
||||||
this.addOtherPlayer(player);
|
this.addOtherPlayer(player);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
this.worldActions.onJoined = (player) => {
|
this.worldActions.onJoined = (player) => {
|
||||||
this.addOtherPlayer(player);
|
this.addOtherPlayer(player);
|
||||||
}
|
};
|
||||||
|
|
||||||
this.worldActions.onMoved = (data) => {
|
this.worldActions.onMoved = (data) => {
|
||||||
const sprite = this.otherPlayers.get(data.socketId);
|
const sprite = this.otherPlayers.get(data.socketId);
|
||||||
if (sprite) {
|
if(sprite) {
|
||||||
this.moveOtherPlayer(sprite, data.x, data.y);
|
this.moveOtherPlayer(sprite, data.x, data.y);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -226,149 +226,147 @@ class Game {
|
|||||||
this.worldActions.onLeft = (data) => {
|
this.worldActions.onLeft = (data) => {
|
||||||
const sprite = this.otherPlayers.get(data.socketId);
|
const sprite = this.otherPlayers.get(data.socketId);
|
||||||
if(sprite) this.otherPlayers.delete(data.socketId);
|
if(sprite) this.otherPlayers.delete(data.socketId);
|
||||||
}
|
};
|
||||||
this.worldActions.onError = (error) => console.error('❌ World error:', error);
|
this.worldActions.onError = (error) => console.error('❌ World error:', error);
|
||||||
|
|
||||||
console.log('✨ Game initializing...');
|
console.log('✨ Game initializing...');
|
||||||
} catch (error) {
|
} catch(error) {
|
||||||
console.error('❌ Failed to initialize game:', error);
|
console.error('❌ Failed to initialize game:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addOtherPlayer(player) {
|
addOtherPlayer(player) {
|
||||||
const sprite = createPet(player.x, player.y, player.name);
|
const sprite = createPet(player.x, player.y, player.name);
|
||||||
sprite.alpha = 0.7;
|
sprite.alpha = 0.7;
|
||||||
this.otherPlayers.set(player.socketId, sprite);
|
this.otherPlayers.set(player.socketId, sprite);
|
||||||
this.app.stage.addChild(sprite);
|
this.app.stage.addChild(sprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
moveOtherPlayer(sprite, targetX, targetY) {
|
moveOtherPlayer(sprite, targetX, targetY) {
|
||||||
const targetPos = isoToScreen(targetX, targetY);
|
const targetPos = isoToScreen(targetX, targetY);
|
||||||
|
|
||||||
const startX = sprite.x;
|
const startX = sprite.x;
|
||||||
const startY = sprite.y;
|
const startY = sprite.y;
|
||||||
let progress = 0;
|
let progress = 0;
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
progress += 0.08;
|
progress += 0.08;
|
||||||
if (progress >= 1) {
|
if(progress >= 1) {
|
||||||
sprite.x = targetPos.x;
|
sprite.x = targetPos.x;
|
||||||
sprite.y = targetPos.y;
|
sprite.y = targetPos.y;
|
||||||
sprite.gridX = targetX;
|
sprite.gridX = targetX;
|
||||||
sprite.gridY = targetY;
|
sprite.gridY = targetY;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sprite.x = startX + (targetPos.x - startX) * progress;
|
sprite.x = startX + (targetPos.x - startX) * progress;
|
||||||
sprite.y = startY + (targetPos.y - startY) * progress;
|
sprite.y = startY + (targetPos.y - startY) * progress;
|
||||||
|
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
|
|
||||||
animate();
|
animate();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeRenderer() {
|
initializeRenderer() {
|
||||||
this.app = new PIXI.Application({
|
this.app = new PIXI.Application({
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
backgroundAlpha: 0,
|
backgroundAlpha: 0,
|
||||||
antialias: true,
|
antialias: true,
|
||||||
resolution: window.devicePixelRatio || 1,
|
resolution: window.devicePixelRatio || 1,
|
||||||
autoDensity: true
|
autoDensity: true
|
||||||
});
|
});
|
||||||
document.getElementById('game').appendChild(this.app.view);
|
document.getElementById('game').appendChild(this.app.view);
|
||||||
|
|
||||||
const tiles = new PIXI.Container();
|
const tiles = new PIXI.Container();
|
||||||
this.app.stage.addChild(tiles);
|
this.app.stage.addChild(tiles);
|
||||||
|
|
||||||
this.world.tiles.forEach(tileData => {
|
this.world.tiles.forEach(tileData => {
|
||||||
const tile = createTile(tileData, this.world.theme);
|
const tile = createTile(tileData, this.world.theme);
|
||||||
tile.on('pointerdown', () => this.movePetTo(tile.gridX, tile.gridY));
|
tile.on('pointerdown', () => this.movePetTo(tile.gridX, tile.gridY));
|
||||||
tiles.addChild(tile);
|
tiles.addChild(tile);
|
||||||
});
|
});
|
||||||
|
|
||||||
const spawn = this.world.tiles.find(t => t.type === 'spawn');
|
const spawn = this.world.tiles.find(t => t.type === 'spawn');
|
||||||
this.pet = createPet(spawn.x, spawn.y, this.playerInfo.name);
|
this.pet = createPet(spawn.x, spawn.y, this.playerInfo.name);
|
||||||
this.app.stage.addChild(this.pet);
|
this.app.stage.addChild(this.pet);
|
||||||
|
|
||||||
this.dialogue = document.getElementById('llm');
|
this.dialogue = document.getElementById('llm');
|
||||||
|
|
||||||
this.setupInput();
|
this.setupInput();
|
||||||
this.app.ticker.add(() => this.gameLoop());
|
this.app.ticker.add(() => this.gameLoop());
|
||||||
}
|
}
|
||||||
|
|
||||||
movePetTo(targetX, targetY) {
|
movePetTo(targetX, targetY) {
|
||||||
if (this.isMoving ||
|
if(this.isMoving || targetX < 0 || targetX >= this.world.gridSize || targetY < 0 || targetY >= this.world.gridSize) return;
|
||||||
targetX < 0 || targetX >= this.world.gridSize ||
|
|
||||||
targetY < 0 || targetY >= this.world.gridSize) return;
|
|
||||||
|
|
||||||
this.isMoving = true;
|
this.isMoving = true;
|
||||||
const targetPos = isoToScreen(targetX, targetY);
|
const targetPos = isoToScreen(targetX, targetY);
|
||||||
|
|
||||||
const startX = this.pet.x;
|
const startX = this.pet.x;
|
||||||
const startY = this.pet.y;
|
const startY = this.pet.y;
|
||||||
let progress = 0;
|
let progress = 0;
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
progress += 0.08;
|
progress += 0.08;
|
||||||
if (progress >= 1) {
|
if(progress >= 1) {
|
||||||
this.pet.x = targetPos.x;
|
this.pet.x = targetPos.x;
|
||||||
this.pet.y = targetPos.y;
|
this.pet.y = targetPos.y;
|
||||||
this.pet.gridX = targetX;
|
this.pet.gridX = targetX;
|
||||||
this.pet.gridY = targetY;
|
this.pet.gridY = targetY;
|
||||||
this.isMoving = false;
|
this.isMoving = false;
|
||||||
|
|
||||||
// Use API action to send move 📤
|
// Use API action to send move 📤
|
||||||
if (this.worldActions) {
|
if(this.worldActions) {
|
||||||
this.worldActions.move(targetX, targetY);
|
this.worldActions.move(targetX, targetY);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pet.x = startX + (targetPos.x - startX) * progress;
|
this.pet.x = startX + (targetPos.x - startX) * progress;
|
||||||
this.pet.y = startY + (targetPos.y - startY) * progress;
|
this.pet.y = startY + (targetPos.y - startY) * progress;
|
||||||
|
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
|
|
||||||
animate();
|
animate();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupInput() {
|
setupInput() {
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if (this.dialogue.isOpen) return;
|
if(this.dialogue.isOpen) return;
|
||||||
|
|
||||||
this.keys[e.key.toLowerCase()] = true;
|
this.keys[e.key.toLowerCase()] = true;
|
||||||
|
|
||||||
if (!this.isMoving) {
|
if(!this.isMoving) {
|
||||||
let newX = this.pet.gridX;
|
let newX = this.pet.gridX;
|
||||||
let newY = this.pet.gridY;
|
let newY = this.pet.gridY;
|
||||||
|
|
||||||
if (this.keys['w'] || this.keys['arrowup']) {
|
if(this.keys['w'] || this.keys['arrowup']) {
|
||||||
newY--;
|
newY--;
|
||||||
} else if (this.keys['s'] || this.keys['arrowdown']) {
|
} else if(this.keys['s'] || this.keys['arrowdown']) {
|
||||||
newY++;
|
newY++;
|
||||||
} else if (this.keys['a'] || this.keys['arrowleft']) {
|
} else if(this.keys['a'] || this.keys['arrowleft']) {
|
||||||
newX--;
|
newX--;
|
||||||
} else if (this.keys['d'] || this.keys['arrowright']) {
|
} else if(this.keys['d'] || this.keys['arrowright']) {
|
||||||
newX++;
|
newX++;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.movePetTo(newX, newY);
|
this.movePetTo(newX, newY);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('keyup', (e) => {
|
window.addEventListener('keyup', (e) => {
|
||||||
this.keys[e.key.toLowerCase()] = false;
|
this.keys[e.key.toLowerCase()] = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
gameLoop() {
|
gameLoop() {
|
||||||
if (!this.isMoving && this.pet) {
|
if(!this.isMoving && this.pet) {
|
||||||
this.pet.children[0].y = -30 + Math.sin(Date.now() / 300) * 2;
|
this.pet.children[0].y = -30 + Math.sin(Date.now() / 300) * 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>NetNavi v1.0.0</title>
|
<title>NetNavi v1.0.0</title>
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png"/>
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
<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://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>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/7.3.2/pixi.min.js"></script>
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Navi from './navi.mjs';
|
import Navi from './navi.mjs';
|
||||||
|
|
||||||
window.navi = new Navi();
|
window.navi = new Navi();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="/components/jukebox.mjs"></script>
|
<script type="module" src="/components/jukebox.mjs"></script>
|
||||||
|
|||||||
258
public/navi.mjs
258
public/navi.mjs
@@ -1,4 +1,4 @@
|
|||||||
export default class Navi {
|
class Navi {
|
||||||
api;
|
api;
|
||||||
connected = false;
|
connected = false;
|
||||||
icon;
|
icon;
|
||||||
@@ -12,11 +12,11 @@ export default class Navi {
|
|||||||
#secret;
|
#secret;
|
||||||
#world;
|
#world;
|
||||||
|
|
||||||
constructor(api = window.location.origin, secret = '') {
|
constructor(api = window.location.origin, secret = '') {
|
||||||
this.api = api.replace(/\/$/, '');
|
this.api = api.replace(/\/$/, '');
|
||||||
this.icon = `${this.api}/favicon.png`;
|
this.icon = `${this.api}/favicon.png`;
|
||||||
this.#secret = secret;
|
this.#secret = secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if(this.#init) return this.#init;
|
if(this.#init) return this.#init;
|
||||||
@@ -56,7 +56,7 @@ export default class Navi {
|
|||||||
this.connected = true;
|
this.connected = true;
|
||||||
this.emit('init');
|
this.emit('init');
|
||||||
res(this);
|
res(this);
|
||||||
} catch (err) {
|
} catch(err) {
|
||||||
rej(err);
|
rej(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -86,41 +86,49 @@ export default class Navi {
|
|||||||
callbacks.forEach(cb => cb(data));
|
callbacks.forEach(cb => cb(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// REST API
|
// REST API
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
async sendUserMessage(userId, message) {
|
async sendUserMessage(userId, message) {
|
||||||
const response = await fetch(`${this.api}/api/message`, {
|
const response = await fetch(`${this.api}/api/message`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({ message })
|
body: JSON.stringify({message})
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Message failed to send bestie');
|
if(!response.ok) throw new Error('Message failed to send bestie');
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
this.emit('message:sent', { userId, message, result });
|
this.emit('message:sent', {
|
||||||
return result;
|
userId,
|
||||||
}
|
message,
|
||||||
|
result
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async linkPet(petId, targetApiUrl) {
|
async linkPet(petId, targetApiUrl) {
|
||||||
const response = await fetch(`${this.api}/api/link`, {
|
const response = await fetch(`${this.api}/api/link`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({ targetApiUrl })
|
body: JSON.stringify({targetApiUrl})
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Link connection is bussin (negatively)');
|
if(!response.ok) throw new Error('Link connection is bussin (negatively)');
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
this.emit('pet:linked', { petId, targetApiUrl, result });
|
this.emit('pet:linked', {
|
||||||
return result;
|
petId,
|
||||||
}
|
targetApiUrl,
|
||||||
|
result
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// WORLD SOCKET
|
// WORLD SOCKET
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
connect(apiOrWorld, world) {
|
connect(apiOrWorld, world) {
|
||||||
let api;
|
let api;
|
||||||
if(world) {
|
if(world) {
|
||||||
api = apiOrWorld;
|
api = apiOrWorld;
|
||||||
@@ -129,12 +137,12 @@ export default class Navi {
|
|||||||
world = apiOrWorld;
|
world = apiOrWorld;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this.world) this.world = {};
|
if(!this.world) this.world = {};
|
||||||
if(this.#world && this.world.api !== api) {
|
if(this.#world && this.world.api !== api) {
|
||||||
this.#world.disconnect();
|
this.#world.disconnect();
|
||||||
this.#world = null;
|
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.api = api;
|
||||||
this.world.data = null;
|
this.world.data = null;
|
||||||
this.world.name = world;
|
this.world.name = world;
|
||||||
@@ -142,113 +150,127 @@ export default class Navi {
|
|||||||
|
|
||||||
const callbacks = {
|
const callbacks = {
|
||||||
move: (x, y) => {
|
move: (x, y) => {
|
||||||
this.#world.emit('move', {x, y});
|
this.#world.emit('move', {
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
});
|
||||||
},
|
},
|
||||||
leave: () => {
|
leave: () => {
|
||||||
this.#world.disconnect();
|
this.#world.disconnect();
|
||||||
this.world = null;
|
this.world = null;
|
||||||
},
|
},
|
||||||
onData: (data) => { },
|
onData: (data) => {
|
||||||
onPlayers: (players) => { },
|
},
|
||||||
onJoined: (player) => { },
|
onPlayers: (players) => {
|
||||||
onMoved: (player) => { },
|
},
|
||||||
onLeft: (player) => { },
|
onJoined: (player) => {
|
||||||
onError: (error) => { }
|
},
|
||||||
}
|
onMoved: (player) => {
|
||||||
|
},
|
||||||
|
onLeft: (player) => {
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.#world.on('data', (data) => {
|
this.#world.on('data', (data) => {
|
||||||
this.world.data = data;
|
this.world.data = data;
|
||||||
callbacks.onData(data);
|
callbacks.onData(data);
|
||||||
this.emit('world:data', data);
|
this.emit('world:data', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#world.on('players', (players) => {
|
this.#world.on('players', (players) => {
|
||||||
this.world.players.clear();
|
this.world.players.clear();
|
||||||
players.forEach(p => this.world.players.set(p.socketId, p));
|
players.forEach(p => this.world.players.set(p.socketId, p));
|
||||||
callbacks.onPlayers(players);
|
callbacks.onPlayers(players);
|
||||||
this.emit('world:players', players)
|
this.emit('world:players', players);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#world.on('joined', (player) => {
|
this.#world.on('joined', (player) => {
|
||||||
this.world.players.set(player.socketId, player);
|
this.world.players.set(player.socketId, player);
|
||||||
callbacks.onJoined(player);
|
callbacks.onJoined(player);
|
||||||
this.emit('world:joined', player);
|
this.emit('world:joined', player);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#world.on('moved', (data) => {
|
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) => {
|
|
||||||
const player = this.world.players.get(data.socketId);
|
const player = this.world.players.get(data.socketId);
|
||||||
this.world.players.delete(data.socketId);
|
if(player) {
|
||||||
callbacks.onLeft(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.emit('world:left', player);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#world.on('error', (error) => {
|
this.#world.on('error', (error) => {
|
||||||
console.error('World error:', error);
|
console.error('World error:', error);
|
||||||
callbacks.onError(error);
|
callbacks.onError(error);
|
||||||
this.emit('world:error', error);
|
this.emit('world:error', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#world.emit('join', {world, api});
|
this.#world.emit('join', {
|
||||||
|
world,
|
||||||
|
api
|
||||||
|
});
|
||||||
|
|
||||||
this.emit('world:join', world);
|
this.emit('world:join', world);
|
||||||
return callbacks;
|
return callbacks;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// LLM
|
// LLM
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
ask(message, stream) {
|
ask(message, stream) {
|
||||||
this.llmCallback = stream;
|
this.llmCallback = stream;
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise((resolve, reject) => {
|
||||||
this.llmResolve = resolve;
|
this.llmResolve = resolve;
|
||||||
this.llmReject = reject;
|
this.llmReject = reject;
|
||||||
this.#socket.emit('llm-ask', {message});
|
this.#socket.emit('llm-ask', {message});
|
||||||
this.emit('llm:ask')
|
this.emit('llm:ask');
|
||||||
});
|
});
|
||||||
|
|
||||||
promise.abort = () => {
|
promise.abort = () => {
|
||||||
this.#socket.emit('llm-abort');
|
this.#socket.emit('llm-abort');
|
||||||
if(this.llmReject) this.llmReject(new Error('Aborted by user'));
|
if(this.llmReject) this.llmReject(new Error('Aborted by user'));
|
||||||
this.llmCallback = null;
|
this.llmCallback = null;
|
||||||
this.llmResolve = null;
|
this.llmResolve = null;
|
||||||
this.llmReject = null;
|
this.llmReject = null;
|
||||||
this.emit('llm:abort')
|
this.emit('llm:abort');
|
||||||
};
|
};
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearChat() {
|
clearChat() {
|
||||||
if(this.#socket) this.#socket.emit('llm-clear');
|
if(this.#socket) this.#socket.emit('llm-clear');
|
||||||
this.emit('llm:clear');
|
this.emit('llm:clear');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// UTILITY
|
// UTILITY
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.icon = this.info = this.theme = this.world = this.#init = this.#secret = null;
|
this.icon = this.info = this.theme = this.world = this.#init = this.#secret = null;
|
||||||
if(this.#world) {
|
if(this.#world) {
|
||||||
this.#world.disconnect();
|
this.#world.disconnect();
|
||||||
this.#world = null;
|
this.#world = null;
|
||||||
}
|
}
|
||||||
if(this.#socket) {
|
if(this.#socket) {
|
||||||
this.#socket.disconnect();
|
this.#socket.disconnect();
|
||||||
this.#socket = null;
|
this.#socket = null;
|
||||||
}
|
}
|
||||||
this.emit('disconnected');
|
this.emit('disconnected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Navi;
|
||||||
|
|||||||
300
src/server.js
300
src/server.js
@@ -1,12 +1,34 @@
|
|||||||
import express from 'express';
|
import express
|
||||||
import { createServer } from 'http';
|
from 'express';
|
||||||
import { Server } from 'socket.io';
|
import {
|
||||||
import cors from 'cors';
|
createServer
|
||||||
import fs from 'fs';
|
} from 'http';
|
||||||
import { join, dirname } from 'path';
|
import {
|
||||||
import { fileURLToPath } from 'url';
|
Server
|
||||||
import {Ai, DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool} from '@ztimson/ai-utils';
|
} from 'socket.io';
|
||||||
import {contrast, shadeColor} from '@ztimson/utils';
|
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
|
// Settings
|
||||||
@@ -38,67 +60,77 @@ function calcColors(theme) {
|
|||||||
mutedContrast: contrast(theme.muted),
|
mutedContrast: contrast(theme.muted),
|
||||||
mutedDark: shadeColor(theme.muted, -.1),
|
mutedDark: shadeColor(theme.muted, -.1),
|
||||||
mutedLight: shadeColor(theme.muted, .1),
|
mutedLight: shadeColor(theme.muted, .1),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let memories = [], settings = {
|
let memories = [],
|
||||||
name: 'Navi',
|
settings = {
|
||||||
personality: 'You are inquisitive about your user trying to best adjust your personally to fit them',
|
name: 'Navi',
|
||||||
instructions: '',
|
personality: 'You are inquisitive about your user trying to best adjust your personally to fit them',
|
||||||
theme: {
|
instructions: '',
|
||||||
background: '#fff',
|
theme: {
|
||||||
border: '#000',
|
background: '#fff',
|
||||||
text: '#252525',
|
border: '#000',
|
||||||
primary: '#9f32ef',
|
text: '#252525',
|
||||||
accent: '#6f16c3',
|
primary: '#9f32ef',
|
||||||
muted: '#a8a8a8',
|
accent: '#6f16c3',
|
||||||
}
|
muted: '#a8a8a8',
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
try {
|
try {
|
||||||
settings = {
|
settings = {
|
||||||
...settings,
|
...settings,
|
||||||
...JSON.parse(fs.readFileSync(settingsFile, 'utf-8'))
|
...JSON.parse(fs.readFileSync(settingsFile, 'utf-8'))
|
||||||
};
|
};
|
||||||
} catch { }
|
} catch { }
|
||||||
try {
|
try {
|
||||||
memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8'));
|
memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8'));
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
if(!updated) return;
|
if(!updated) return;
|
||||||
updated = false;
|
updated = false;
|
||||||
|
|
||||||
const dir = dirname(settingsFile);
|
const dir = dirname(settingsFile);
|
||||||
if (!fs.existsSync(dir)) {
|
if(!fs.existsSync(dir)) {
|
||||||
fs.mkdir(dir, { recursive: true }, (err) => {
|
fs.mkdir(dir, {recursive: true}, (err) => {
|
||||||
if (err) throw err; // Fail loudly if dirs can’t be made 💀
|
if(err) throw err; // Fail loudly if dirs can’t be made 💀
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
||||||
fs.writeFileSync(memoriesFile, JSON.stringify(memories, null, 2));
|
fs.writeFileSync(memoriesFile, JSON.stringify(memories, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
const ai = new Ai({
|
const ai = new Ai({
|
||||||
llm: {
|
llm: {
|
||||||
models: {
|
models: {
|
||||||
'Ministral-3': {proto: 'openai', host: 'http://10.69.0.55:11728', token: 'ignore'},
|
'Ministral-3': {
|
||||||
},
|
proto: 'openai',
|
||||||
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 || ''}`,
|
host: 'http://10.69.0.55:11728',
|
||||||
tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, {
|
token: 'ignore'
|
||||||
name: 'adjust_personality',
|
},
|
||||||
description: 'Replace your current personality instructions',
|
},
|
||||||
args: {instructions: {type: 'string', description: 'Bullet point list of how to behave'}},
|
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 || ''}`,
|
||||||
fn: (args) => {
|
tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, {
|
||||||
settings.personality = args.instructions;
|
name: 'adjust_personality',
|
||||||
updated = true;
|
description: 'Replace your current personality instructions',
|
||||||
return 'done!';
|
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 app = express();
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
const io = new Server(httpServer, {
|
const io = new Server(httpServer, {
|
||||||
cors: {origin: "*", methods: ["GET", "POST"]}
|
cors: {
|
||||||
|
origin: '*',
|
||||||
|
methods: ['GET', 'POST']
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
@@ -123,42 +158,42 @@ const chatHistory = new Map();
|
|||||||
io.on('connection', (socket) => {
|
io.on('connection', (socket) => {
|
||||||
console.debug('👤 User connected:', socket.id);
|
console.debug('👤 User connected:', socket.id);
|
||||||
|
|
||||||
chatHistory.set(socket.id, []);
|
chatHistory.set(socket.id, []);
|
||||||
let currentRequest = null;
|
let currentRequest = null;
|
||||||
|
|
||||||
socket.on('llm-clear', async () => {
|
socket.on('llm-clear', async () => {
|
||||||
chatHistory.set(socket.id, []);
|
chatHistory.set(socket.id, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('llm-abort', () => {
|
socket.on('llm-abort', () => {
|
||||||
if (currentRequest?.abort) {
|
if(currentRequest?.abort) {
|
||||||
currentRequest.abort();
|
currentRequest.abort();
|
||||||
currentRequest = null;
|
currentRequest = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('llm-ask', async (data) => {
|
socket.on('llm-ask', async (data) => {
|
||||||
const { message } = data;
|
const {message} = data;
|
||||||
const history = chatHistory.get(socket.id);
|
const history = chatHistory.get(socket.id);
|
||||||
currentRequest = ai.language.ask(message, {
|
currentRequest = ai.language.ask(message, {
|
||||||
history,
|
history,
|
||||||
memory: memories,
|
memory: memories,
|
||||||
stream: (chunk) => socket.emit('llm-stream', chunk)
|
stream: (chunk) => socket.emit('llm-stream', chunk)
|
||||||
}).then(resp => {
|
}).then(resp => {
|
||||||
chatHistory.set(socket.id, history);
|
chatHistory.set(socket.id, history);
|
||||||
socket.emit('llm-response', { message: resp });
|
socket.emit('llm-response', {message: resp});
|
||||||
updated = true;
|
updated = true;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
socket.emit('llm-error', {message: err.message || err.toString()});
|
socket.emit('llm-error', {message: err.message || err.toString()});
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
currentRequest = null;
|
currentRequest = null;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
console.log('👤 User disconnected:', socket.id);
|
console.log('👤 User disconnected:', socket.id);
|
||||||
chatHistory.delete(socket.id);
|
chatHistory.delete(socket.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -168,22 +203,26 @@ const worldPlayers = new Map();
|
|||||||
|
|
||||||
// Load world data
|
// Load world data
|
||||||
function loadWorld(name) {
|
function loadWorld(name) {
|
||||||
let w, t;
|
let w,
|
||||||
|
t;
|
||||||
try {
|
try {
|
||||||
w = JSON.parse(fs.readFileSync(join(worlds, (name || 'home') + '.json'), 'utf-8'));
|
w = JSON.parse(fs.readFileSync(join(worlds, (name || 'home') + '.json'), 'utf-8'));
|
||||||
} catch (error) {
|
} catch(error) {
|
||||||
console.error(`Failed to load world ${name}:`, error);
|
console.error(`Failed to load world ${name}:`, error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
t = JSON.parse(fs.readFileSync(join(protocols, w.theme + '.json'), 'utf-8'));
|
t = JSON.parse(fs.readFileSync(join(protocols, w.theme + '.json'), 'utf-8'));
|
||||||
t.colors = calcColors(t.colors);
|
t.colors = calcColors(t.colors);
|
||||||
} catch (error) {
|
} catch(error) {
|
||||||
console.error(`Failed to load theme protocol ${w.theme}:`, error);
|
console.error(`Failed to load theme protocol ${w.theme}:`, error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
worldPlayers.set(name, new Map());
|
worldPlayers.set(name, new Map());
|
||||||
return {...w, theme: t};
|
return {
|
||||||
|
...w,
|
||||||
|
theme: t
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
io.of('/world').on('connection', (socket) => {
|
io.of('/world').on('connection', (socket) => {
|
||||||
@@ -194,7 +233,10 @@ io.of('/world').on('connection', (socket) => {
|
|||||||
|
|
||||||
// Join a world
|
// Join a world
|
||||||
socket.on('join', async (data) => {
|
socket.on('join', async (data) => {
|
||||||
const { world, api } = data;
|
const {
|
||||||
|
world,
|
||||||
|
api
|
||||||
|
} = data;
|
||||||
const info = await fetch(api.replace(/\/$/, '') + '/api/info').then(resp => {
|
const info = await fetch(api.replace(/\/$/, '') + '/api/info').then(resp => {
|
||||||
if(resp.ok) return resp.json();
|
if(resp.ok) return resp.json();
|
||||||
socket.emit('error', {message: `Invalid Navi API: ${api}`});
|
socket.emit('error', {message: `Invalid Navi API: ${api}`});
|
||||||
@@ -202,7 +244,7 @@ io.of('/world').on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const worldData = loadWorld(world);
|
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
|
// Leave previous world if any
|
||||||
if(currentWorld) {
|
if(currentWorld) {
|
||||||
@@ -236,10 +278,17 @@ io.of('/world').on('connection', (socket) => {
|
|||||||
// Player movement
|
// Player movement
|
||||||
socket.on('move', (data) => {
|
socket.on('move', (data) => {
|
||||||
if(!currentWorld || !playerData) return;
|
if(!currentWorld || !playerData) return;
|
||||||
const { x, y } = data;
|
const {
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
} = data;
|
||||||
playerData.x = x;
|
playerData.x = x;
|
||||||
playerData.y = y;
|
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
|
// Disconnect
|
||||||
@@ -260,12 +309,12 @@ io.of('/world').on('connection', (socket) => {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
app.get('/favicon.*', (req, res) => {
|
app.get('/favicon.*', (req, res) => {
|
||||||
res.sendFile(logoFile);
|
res.sendFile(logoFile);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get Navi info
|
// Get Navi info
|
||||||
app.get('/api/info', (req, res) => {
|
app.get('/api/info', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
name: settings.name,
|
name: settings.name,
|
||||||
theme: calcColors(settings.theme),
|
theme: calcColors(settings.theme),
|
||||||
});
|
});
|
||||||
@@ -273,41 +322,56 @@ app.get('/api/info', (req, res) => {
|
|||||||
|
|
||||||
// Get sprite sheet
|
// Get sprite sheet
|
||||||
app.get('/api/sprite', (req, res) => {
|
app.get('/api/sprite', (req, res) => {
|
||||||
const { petId } = req.params;
|
const {petId} = req.params;
|
||||||
// TODO: Return actual sprite sheet URL
|
// TODO: Return actual sprite sheet URL
|
||||||
res.json({
|
res.json({
|
||||||
spriteUrl: '/sprites/default-pet.png',
|
spriteUrl: '/sprites/default-pet.png',
|
||||||
frameWidth: 32,
|
frameWidth: 32,
|
||||||
frameHeight: 32,
|
frameHeight: 32,
|
||||||
animations: {
|
animations: {
|
||||||
idle: { frames: [0, 1, 2, 3], speed: 200 },
|
idle: {
|
||||||
walk: { frames: [4, 5, 6, 7], speed: 100 }
|
frames: [0, 1, 2, 3],
|
||||||
}
|
speed: 200
|
||||||
});
|
},
|
||||||
|
walk: {
|
||||||
|
frames: [4, 5, 6, 7],
|
||||||
|
speed: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send message to user (push notification / email / etc)
|
// Send message to user (push notification / email / etc)
|
||||||
app.post('/api/message', (req, res) => {
|
app.post('/api/message', (req, res) => {
|
||||||
const { userId } = req.params;
|
const {userId} = req.params;
|
||||||
const { message } = req.body;
|
const {message} = req.body;
|
||||||
// TODO: Implement notification system
|
// TODO: Implement notification system
|
||||||
res.json({ success: true, message: 'Message sent' });
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Message sent'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send message to PET LLM
|
// Send message to PET LLM
|
||||||
app.post('/api/message', (req, res) => {
|
app.post('/api/message', (req, res) => {
|
||||||
const { petId } = req.params;
|
const {petId} = req.params;
|
||||||
const { message } = req.body;
|
const {message} = req.body;
|
||||||
// TODO: Queue message for LLM processing
|
// TODO: Queue message for LLM processing
|
||||||
res.json({ success: true, message: 'Message queued' });
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Message queued'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Link another PET
|
// Link another PET
|
||||||
app.post('/api/link', (req, res) => {
|
app.post('/api/link', (req, res) => {
|
||||||
const { petId } = req.params;
|
const {petId} = req.params;
|
||||||
const { targetApiUrl } = req.body;
|
const {targetApiUrl} = req.body;
|
||||||
// TODO: Store link in database
|
// TODO: Store link in database
|
||||||
res.json({success: true, message: 'PET linked'});
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'PET linked'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -317,7 +381,7 @@ const PORT = process.env.PORT || 3000;
|
|||||||
|
|
||||||
setInterval(() => save(), 5 * 60_000);
|
setInterval(() => save(), 5 * 60_000);
|
||||||
httpServer.listen(PORT, () => {
|
httpServer.listen(PORT, () => {
|
||||||
console.log(`🚀 Server running on: http://localhost:${PORT}`);
|
console.log(`🚀 Server running on: http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
function shutdown() {
|
function shutdown() {
|
||||||
|
|||||||
Reference in New Issue
Block a user