Первый коммит после распаковки архива

This commit is contained in:
2025-08-14 20:14:42 +03:00
commit 5d4e9ba201
354 changed files with 40492 additions and 0 deletions

3
.env Normal file
View File

@@ -0,0 +1,3 @@
DATABASE_URL=postgres://my_user:scupAs2s@91.107.120.205:5432/game_db
JWT_SECRET=tgkkkxd2131
DATABASE_URL_VIRTUAL_WORLD=postgres://my_user:scupAs2s@91.107.120.205:5432/virtual_world

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# зависимости npm
node_modules/
# скомпилированные файлы
build/

145
README.md Normal file
View File

@@ -0,0 +1,145 @@
# EEV\_Proj Server
Этот репозиторий содержит серверную часть проекта **EEV\_Proj** (ветка `server`), реализованную с использованием Node.js и Express. Сервер отвечает за API, взаимодействует с базой данных и обслуживает статические файлы фронтенда.
## Оглавление
1. [Описание проекта](#описание-проекта)
2. [Технологии](#технологии)
3. [Установка и запуск](#установка-и-запуск)
* [Клонирование](#клонирование)
* [Установка зависимостей](#установка-зависимостей)
* [Настройка окружения](#настройка-окружения)
* [Запуск проекта](#запуск-проекта)
4. [Структура проекта](#структура-проекта)
5. [Основные модули](#основные-модули)
6. [PM2 конфигурация](#pm2-конфигурация)
7. [Контакты](#контакты)
## Описание проекта
Проект **EEV\_Proj** представляет собой веб-приложение с клиентской и серверной частями. Ветка `server` содержит сервер, который:
* Обрабатывает HTTP-запросы и предоставляет REST API.
* Подключается к базе данных для хранения и извлечения данных.
* Раздаёт статические файлы фронтенда (HTML/CSS/JS).
* Поддерживает кластерный запуск через PM2.
## Технологии
* **Node.js** — среда выполнения JavaScript.
* **Express** — веб-фреймворк для Node.js.
* **PM2** — процесс-менеджер для Node.js приложений.
* **MySQL / PostgreSQL / MongoDB** (в зависимости от вашей настройки) — СУБД.
* **JavaScript (ES6+)** — основной язык разработки.
## Установка и запуск
### Клонирование
```bash
git clone https://github.com/IprokK/EEV_Proj.git
cd EEV_Proj
git checkout server
```
### Установка зависимостей
```bash
npm install
```
### Настройка окружения
Создайте файл `.env` в корне проекта и добавьте необходимые переменные:
```dotenv
# Сервер
PORT=3000
# База данных
DB_HOST=localhost
DB_PORT=5432
DB_USER=your_username
DB_PASS=your_password
DB_NAME=your_database
```
### Запуск проекта
* **Обычный запуск**
```bash
node server.js
```
* **Через PM2**
```bash
pm2 start ecosystem.config.js --env production
```
Откройте браузер и перейдите по адресу `http://localhost:<PORT>/`.
## Структура проекта
```
EEV_Proj/
├── public/ # Статические файлы фронтенда (HTML, CSS, JS)
├── src/ # Исходники клиентской части:
│ ├── index.html # Основной HTML-шаблон приложения
│ ├── index.js # Точка входа React: монтирует корневой компонент
│ ├── App.js # Корневой React-компонент приложения
│ ├── components/ # Папка переиспользуемых React-компонентов
│ │ └── … # Компоненты (например, Header, Footer, Dashboard и т.д.)
│ ├── services/ # Модуль для работы с API (REST-запросы к серверу)
│ │ └── api.js # Пример файла с конфигурацией запросов
│ ├── utils/ # Утилитарные функции и хелперы (валидация, форматирование)
│ ├── styles/ # Глобальные стили и темы (CSS/SCSS файлы)
│ └── assets/ # Статические ресурсы (изображения, шрифты)
├── .env # Переменные окружения (не хранится в репозитории)
├── db.js # Модуль подключения к базе данных
├── server.js # Точка входа сервера (Express application)
├── ecosystem.config.js # Конфигурация для PM2
├── package.json # Описание зависимостей и npm-скрипты
└── README.md # Описание проекта (вы находитесь здесь)
```
## Основные модули
* **server.js** — стартовый файл, инициализирует Express, настраивает middleware, роуты и запускает сервер.
* **db.js** — конфигурирует подключение к базе данных через выбранный драйвер.
* **public/** — содержит готовый к раздаче фронтенд (сборка или статические файлы).
* **src/** — содержит исходники клиентской части (React-компоненты, сервисы, утилиты).
* **ecosystem.config.js** — позволяет управлять процессом сервера через PM2.
## PM2 конфигурация
Файл `ecosystem.config.js` позволяет управлять процессом сервера в продакшене:
```js
module.exports = {
apps: [{
name: "EEV_Server",
script: "server.js",
env: {
NODE_ENV: "development",
},
env_production: {
NODE_ENV: "production",
}
}]
};
```
Запуск в режиме продакшен:
```bash
pm2 start ecosystem.config.js --env production
pm2 logs EEV_Server
```
## Контакты
Если у вас возникли вопросы или предложения, создайте issue в репозитории или напишите Ilya Nice напрямую.

22
db.js Normal file
View File

@@ -0,0 +1,22 @@
// db.js
require('dotenv').config();
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: false
});
module.exports = {
/**
* Execute a SQL query using the shared connection pool.
* @param {string} text
* @param {any[]} [params]
*/
query: (text, params) => pool.query(text, params),
/**
* Expose the underlying pool for transactions or advanced usages.
*/
pool
};

16
db1.js Normal file
View File

@@ -0,0 +1,16 @@
require('dotenv').config();
const { Pool } = require('pg');
const connectionString =
process.env.DATABASE_URL_VIRTUAL_WORLD || process.env.DATABASE_URL;
const virtualWorldPool = new Pool({
connectionString,
ssl: false
});
module.exports = {
virtualWorldPool: {
query: (text, params) => virtualWorldPool.query(text, params)
}
};

264
economy.js Normal file
View File

@@ -0,0 +1,264 @@
const { readFileSync } = require('fs');
const path = require('path');
const fetch = require('node-fetch');
class Economy {
constructor(io, db) {
this.io = io;
this.db = db;
const cfgPath = path.join(__dirname, 'economy', 'config.json');
this.config = JSON.parse(readFileSync(cfgPath, 'utf8'));
this.batch = new Map();
this.initTables().catch(err => this.log('error', 'initTables', err));
this.ensureTreasuryRows().catch(err => this.log('error', 'ensureTreasury', err));
this.registerSocketHandlers();
this.interval = setInterval(() => this.flushBatch(), this.config.batchIntervalMinutes * 60 * 1000);
}
async initTables() {
await this.db.query(`CREATE TABLE IF NOT EXISTS treasury (
id SERIAL PRIMARY KEY,
country_code TEXT UNIQUE,
balance NUMERIC DEFAULT 0
);`);
await this.db.query(`CREATE TABLE IF NOT EXISTS accounts (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
currency TEXT,
balance NUMERIC
);`);
await this.db.query(`CREATE TABLE IF NOT EXISTS transactions (
id SERIAL PRIMARY KEY,
from_account INTEGER,
to_account INTEGER,
amount NUMERIC,
currency TEXT,
type TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);`);
await this.db.query(`CREATE TABLE IF NOT EXISTS items (
id SERIAL PRIMARY KEY,
key TEXT UNIQUE,
name TEXT NOT NULL,
type TEXT,
weight NUMERIC DEFAULT 1,
hunger_gain NUMERIC DEFAULT 0,
thirst_gain NUMERIC DEFAULT 0,
stackable BOOLEAN DEFAULT true,
functions JSONB DEFAULT '{}'::jsonb
);`);
await this.db.query(`CREATE TABLE IF NOT EXISTS inventory (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
item_id INTEGER,
name TEXT,
quantity INTEGER,
stackable BOOLEAN,
weight NUMERIC
);`);
await this.db.query(
'CREATE UNIQUE INDEX IF NOT EXISTS idx_inventory_user_item ON inventory(user_id, item_id)'
);
await this.db.query(
'ALTER TABLE users ADD COLUMN IF NOT EXISTS satiety NUMERIC DEFAULT 100'
);
await this.db.query(
'ALTER TABLE users ADD COLUMN IF NOT EXISTS thirst NUMERIC DEFAULT 100'
);
}
async ensureTreasuryRows() {
try {
// ищем подходящее поле для "кода страны"
const cand = await this.db.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'countries'
AND column_name IN ('code','iso_code','alpha2','alpha3')
LIMIT 1
`);
const col = cand.rows[0]?.column_name || 'name';
const { rows } = await this.db.query(`SELECT ${col} AS code FROM countries`);
for (const r of rows) {
await this.db.query(
'INSERT INTO treasury(country_code) VALUES($1) ON CONFLICT (country_code) DO NOTHING',
[String(r.code)]
);
}
} catch (e) {
this.log('error', 'ensureTreasuryRows failed', e);
}
}
async createAccount(userId, currency) {
const res = await this.db.query('SELECT balance FROM users WHERE id=$1', [userId]);
const initial = res.rows[0] ? parseFloat(res.rows[0].balance) : this.config.startBalance;
if (!res.rows.length) return;
if (res.rows[0].balance == null) {
await this.db.query('UPDATE users SET balance=$2 WHERE id=$1', [userId, initial]);
}
await this.db.query(
'INSERT INTO accounts(user_id, currency, balance) VALUES($1,$2,$3) ON CONFLICT (user_id, currency) DO NOTHING',
[userId, currency, initial]
);
this.log('info', `Account ensured for user ${userId} with balance ${initial}`);
}
async getBalance(userId, currency) {
this.log('info', 'getBalance', { userId, currency });
const { rows } = await this.db.query('SELECT balance FROM users WHERE id=$1', [userId]);
return rows[0] ? parseFloat(rows[0].balance) : 0;
}
async transfer(fromUser, toUser, amount, currency, type) {
this.log('info', 'transfer begin', { fromUser, toUser, amount, currency, type });
const client = await this.db.pool.connect();
try {
await client.query('BEGIN');
const fromRes = await client.query(
'UPDATE users SET balance = balance - $1 WHERE id=$2 RETURNING balance',
[amount, fromUser]
);
const toRes = await client.query(
'UPDATE users SET balance = balance + $1 WHERE id=$2 RETURNING balance',
[amount, toUser]
);
await client.query(
'INSERT INTO transactions(from_account,to_account,amount,currency,type) VALUES($1,$2,$3,$4,$5)',
[fromUser, toUser, amount, currency, type]
);
await client.query('COMMIT');
this.io.emit('economy:balanceChanged', { userId: fromUser, currency, newBalance: fromRes.rows[0].balance });
this.io.emit('economy:balanceChanged', { userId: toUser, currency, newBalance: toRes.rows[0].balance });
this.io.emit('economy:transactionRecorded', { fromUser, toUser, amount, currency, type });
} catch (e) {
await client.query('ROLLBACK');
this.log('error', 'transfer failed', e);
throw e;
} finally {
client.release();
}
}
convert(amount, fromCurrency, toCurrency) {
this.log('info', 'convert request', { amount, fromCurrency, toCurrency });
this.io.emit('economy:exchangeRateRequested', { fromCurrency, toCurrency });
const rates = this.config.exchangeRates;
const usd = amount / rates[fromCurrency];
const result = usd * rates[toCurrency];
this.io.emit('economy:exchangePerformed', { amount, fromCurrency, toCurrency, result });
return result;
}
async addItem(userId, item) {
await this.db.query(
`INSERT INTO inventory(user_id, item_id, name, quantity, stackable, weight)
VALUES($1,$2,$3,$4,$5,$6)
ON CONFLICT (user_id, item_id) DO UPDATE SET quantity = inventory.quantity + EXCLUDED.quantity`,
[userId, item.item_id, item.name, item.quantity, item.stackable, item.weight]
);
this.log('info', 'addItem', { userId, item });
}
async removeItem(userId, itemId, quantity) {
await this.db.query(
`UPDATE inventory SET quantity = GREATEST(quantity - $3,0) WHERE user_id=$1 AND item_id=$2`,
[userId, itemId, quantity]
);
await this.db.query('DELETE FROM inventory WHERE user_id=$1 AND item_id=$2 AND quantity<=0', [userId, itemId]);
this.log('info', 'removeItem', { userId, itemId, quantity });
}
async getInventory(userId) {
const { rows } = await this.db.query('SELECT * FROM inventory WHERE user_id=$1', [userId]);
return rows;
}
queueUpdate(update) {
const existing = this.batch.get(update.userId) || {};
this.batch.set(update.userId, { ...existing, ...update });
}
async flushBatch() {
for (const upd of this.batch.values()) {
try {
await this.db.query(
'UPDATE users SET health_level = COALESCE($2, health_level), satiety = COALESCE($3, satiety), thirst = COALESCE($4, thirst) WHERE id=$1',
[upd.userId, upd.health, upd.satiety, upd.thirst]
);
} catch (e) {
this.log('error', 'flushBatch error', e);
}
}
this.batch.clear();
}
registerSocketHandlers() {
this.io.on('connection', socket => {
socket.on('economy:getBalance', async ({ userId }) => {
const effectiveUserId = userId ?? socket.userId;
const bal = await this.getBalance(effectiveUserId);
socket.emit('economy:balanceChanged', { userId: effectiveUserId, newBalance: bal });
});
socket.on('economy:transfer', async data => {
try {
await this.transfer(data.fromUser, data.toUser, data.amount, data.currency, data.type);
} catch (e) {
socket.emit('economy:error', { message: 'transfer failed' });
}
});
socket.on('economy:buyItem', async ({ userId, item }) => {
await this.addItem(userId, item);
});
socket.on('economy:getInventory', async ({ userId }) => {
socket.emit('economy:inventory', await this.getInventory(userId));
});
socket.on('economy:removeItem', async ({ userId, itemId, quantity }) => {
await this.removeItem(userId, itemId, quantity);
socket.emit('economy:inventory', await this.getInventory(userId));
});
socket.on('economy:updateStats', data => {
this.queueUpdate({ userId: socket.userId, ...data });
});
socket.on('economy:exchange', ({ amount, fromCurrency, toCurrency }) => {
const result = this.convert(amount, fromCurrency, toCurrency);
socket.emit('economy:exchangeResult', { result });
});
});
}
async log(level, message, meta) {
const entry = { level, message, meta, timestamp: new Date().toISOString() };
console[level === 'error' ? 'error' : 'log'](`[Economy] ${message}`, meta || '');
const url = this.config.monitoringEndpoint;
// отключаем внешний вызов, если endpoint пустой или примерный
if (!url || /(^|\.)example\.com$/.test(new URL(url).hostname)) return;
try {
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry)
});
} catch (e) {
// глушим сетевую ошибку, чтобы не засорять логи
}
}
}
module.exports = Economy;

6
economy/config.json Normal file
View File

@@ -0,0 +1,6 @@
{
"startBalance": 10000,
"batchIntervalMinutes": 5,
"exchangeRates": { "USD": 1, "EUR": 0.9 },
"monitoringEndpoint": "https://monitor.example.com/log"
}

17
ecosystem.config.js Normal file
View File

@@ -0,0 +1,17 @@
module.exports = {
apps: [
{
name: 'threenew-api',
script: 'server.js',
cwd: '/threenew',
env: { NODE_ENV: 'production', PORT: 4000 }
},
{
name: 'threenew-web',
script: 'serve',
cwd: '/threenew',
args: '-s build -l 3000',
env: { NODE_ENV: 'production' }
}
]
}

41
gameTime.js Normal file
View File

@@ -0,0 +1,41 @@
const fs = require('fs');
const path = require('path');
class GameTime {
constructor(io, speed = 8) {
this.io = io;
this.speed = speed;
this.file = path.join(__dirname, 'saves', 'game_time.json');
this.load();
this.timer = setInterval(() => this.tick(), 1000);
}
load() {
try {
const data = JSON.parse(fs.readFileSync(this.file, 'utf8'));
this.gameTime = new Date(data.time);
this.lastReal = data.lastReal;
} catch {
this.gameTime = new Date('2025-01-01T00:00:00Z');
this.lastReal = Date.now();
}
}
save() {
fs.writeFileSync(
this.file,
JSON.stringify({ time: this.gameTime.toISOString(), lastReal: this.lastReal })
);
}
tick() {
const now = Date.now();
const diff = now - this.lastReal;
this.gameTime = new Date(this.gameTime.getTime() + diff * this.speed);
this.lastReal = now;
this.io.emit('gameTime:update', { time: this.gameTime.toISOString() });
this.save();
}
}
module.exports = GameTime;

View File

@@ -0,0 +1,14 @@
warning: in the working copy of '.env', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.gitignore', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'db.js', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'db1.js', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'package-lock.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'server.js', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'src/Game.js', LF will be replaced by CRLF the next time Git touches it
.env | 4 ++--
db.js | 4 +---
db1.js | 2 +-
package-lock.json | 57 +++++++++++++++++++++++++++++--------------------------
server.js | 10 ++++------
src/Game.js | 40 +++++++++++++-------------------------
6 files changed, 51 insertions(+), 66 deletions(-)

31030
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "RevProj",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@react-three/drei": "^9.72.0",
"@react-three/fiber": "8.16.8",
"@readyplayerme/visage": "^6.10.0",
"bcrypt": "^5.1.1",
"compression": "^1.7.4",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"pathfinding": "^0.4.18",
"pg": "^8.15.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^7.5.2",
"react-scripts": "5.0.1",
"socket.io": "^4.8.1",
"socket.io-client": "^4.6.1",
"three": "0.166.1",
"wavesurfer.js": "^7.10.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"overrides": {
"nth-check": "^2.0.1",
"postcss": "^8.4.31"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

Binary file not shown.

BIN
public/audio/firs.ogg Normal file

Binary file not shown.

View File

@@ -0,0 +1,153 @@
{
"name": "Галина",
"avatar": "/images/npc/bartender.jpg",
"filename": "Adventurer.json",
"dialog": [
{
"id": 0,
"text": "Ну? Значит, ты к нам за работой припёрся? Ладно, давай документы...",
"answers": [
{
"text": "Документов нет",
"next": 1
},
{
"text": "Я просто осматриваюсь...",
"next": 2
},
{
"text": "...",
"end": true
}
]
},
{
"id": 1,
"text": "Ах, у тебя их нет? Я бы удивилась другому развитию событий. Тогда придется заполнить их. Писать то хоть умеешь?",
"answers": [
{
"text": "Как-нибудь справлюсь",
"next": "form_data"
},
{
"text": "Может, есть работа без документов?",
"next": 3
}
]
},
{
"id": 2,
"text": "Осматриваешься? Ну-ну... Только не задерживайся слишком долго. В нашем городе без дела болтаться — себе дороже.",
"answers": [
{
"text": "А куда можно пойти?",
"next": 3
},
{
"text": "Ладно, тогда давай документы...",
"next": 1
}
]
},
{
"id": 3,
"text": "Слушай, есть один парень... сидит в кустах за углом. Зовут его Костя Ключник. Он как раз набирает людей для одной работы. Только не говори, что я тебя направила.",
"answers": [
{
"text": "Спасибо, попробую",
"next": 4
},
{
"text": "А что за работа?",
"next": 5
}
]
},
{
"id": 4,
"text": "Ну иди уже, не задерживайся. И смотри в оба — Костя любит шутки, но не над собой.",
"answers": [
{
"end": true
}
]
},
{
"id": 5,
"text": "Если бы я знала все детали, сама бы там работала. Иди сам узнаешь. Если, конечно, не струсишь.",
"answers": [
{
"text": "Ладно, иду",
"next": 4
},
{
"text": "Может, лучше заполню документы?",
"next": "form_data"
}
]
},
{
"id": "form_data",
"type": "form",
"title": "Заполнение анкеты",
"fields": [
{
"name": "skills",
"label": "Твои навыки и перки:",
"type": "text",
"placeholder": "Навыки через запятую",
"required": true
},
{
"name": "work_experience",
"label": "Предыдущие места работы:",
"type": "textarea",
"placeholder": "Где и кем работал",
"required": false
},
{
"name": "background",
"label": "Твое прошлое:",
"type": "textarea",
"placeholder": "Краткая информация о себе",
"required": false
}
],
"submit_text": "Отправить данные",
"next": 6
},
{
"id": 6,
"text": "Так... посмотрим что тут... (долго листает бумаги) Ну ты и говно, дружок. Ладно, есть для тебя один вариант.",
"answers": [
{
"text": "Какой?",
"next": 7
}
]
},
{
"id": 7,
"text": "Слушай сюда. Есть один парень... сидит в кустах за углом. Зовут его Костя Ключник. Он как раз набирает людей. Вот и вся вакансия.",
"answers": [
{
"text": "И это всё?",
"next": 8
},
{
"text": "Ладно, пойду",
"next": 4
}
]
},
{
"id": 8,
"text": "Да, это всё. Ты думал, у нас тут офис белых воротничков? Иди уже, не задерживайся.",
"answers": [
{
"end": true
}
]
}
]
}

View File

@@ -0,0 +1,167 @@
{
"name": "Костя Ключник",
"avatar": "/images/npc/guard.jpg",
"filename": "BeachCharacter.json",
"dialog": [
{
"id": 0,
"text": "(нервно озираясь по сторонам) Тссс... Ты кто такой? Мент? А? Нет? Ну ладно... (быстро чешет нос) Галина говорила? Ну эта... которая в баре...",
"answers": [
{
"text": "Да, она направила",
"next": 1
},
{
"text": "Кто такая Галина?",
"next": 2
}
]
},
{
"id": 1,
"text": "(хихикает) Ага, ну конечно направила... Она у нас вся такая... (внезапно серьёзнеет) Ладно, браток, работа есть. Но сначала... (ковыряет в носу) Ты че, стремаешься?",
"answers": [
{
"text": "Какая работа?",
"next": 3
},
{
"text": "Ты че такой странный?",
"next": 4
}
]
},
{
"id": 2,
"text": "(параноидально оглядывается) Ты че, прикалываешься? Галина! Ну... (делает жест рукой у рта) Которая... Ну в общем... (внезапно меняет тему) Ты хочешь заработать или нет?",
"answers": [
{
"text": "Хочу заработать",
"next": 3
},
{
"text": "Ты точно нормальный?",
"next": 4
}
]
},
{
"id": 3,
"text": "(потирает руки) Оооо, братан, работа огонь! Цифры взламывать будем! (внезапно замолкает, прислушивается) Ты слышал? Нет? Ну ладно... (быстро) Но сначала... (достаёт из кармана пакетик) Хочешь попробовать? Для смелости...",
"answers": [
{
"text": "Давай попробую",
"next": 5
},
{
"text": "Нет, я не употребляю",
"next": 6
}
]
},
{
"id": 4,
"text": "(нервно смеётся) Нормальный? Да я самый нормальный тут! (вдруг серьёзнеет) Вот только вчера... Нет, не буду рассказывать... (глаза бегают) Ты работу хочешь или нет?",
"answers": [
{
"text": "Хочу работу",
"next": 3
},
{
"text": "Ты пугаешь меня",
"next": 7
}
]
},
{
"id": 5,
"text": "(радостно) Ооо, наш человек! (суёт пакетик) На вот, только не всё сразу, а то... (делает широкий жест руками) Бах! И тебя нет! Ха-ха! Ладно, заходи завтра, когда... ну... разберёшься со своими делами. Я тебе всё расскажу.",
"answers": [
{
"text": "Ладно, приду завтра",
"next": 8,
"quest_start": "hack_job"
},
{
"text": "Может лучше прямо сейчас?",
"next": 9
}
]
},
{
"id": 6,
"text": "(разочарованно) Фу, скукота... (пожимает плечами) Ну ладно, работа всё равно есть. Заходи завтра, я тебе всё расскажу. Только... (понижает голос) Никому не говори, ладно?",
"answers": [
{
"text": "Хорошо, приду завтра",
"next": 8,
"quest_start": "hack_job"
},
{
"text": "А можно подробнее?",
"next": 10
}
]
},
{
"id": 7,
"text": "(внезапно злится) Пугаю? Да я тебя... (резко успокаивается) Ладно, братан, иди отсюда. Не для тебя эта работа. (начинает что-то бормотать себе под нос)",
"answers": [
{
"end": true
}
]
},
{
"id": 8,
"text": "(кивает) Молодец. Запомни: подвал за углом, охраннику скажешь... (шёпотом) 'берёзовый сок'. Он тебя пропустит. И... (внезапно хватает за руку) Только никому, понял? Ни-ко-му!",
"answers": [
{
"end": true
}
]
},
{
"id": 9,
"text": "(панически) Сейчас? Нет, нет, нет! (осматривается) Слишком много... глаз. Завтра. Только завтра. (начинает быстро уходить)",
"answers": [
{
"end": true
}
]
},
{
"id": 10,
"text": "(нервно оглядывается) Подробнее? Ну... (понижает голос) Есть подвал. Там компы. Надо... ну... (делает движение пальцами как при печати) Взламывать. Охраннику скажешь 'берёзовый сок' - пропустит. Всё. Больше ничего не знаю. (начинает чесаться)",
"answers": [
{
"text": "Понятно, приду завтра",
"next": 8,
"quest_start": "hack_job"
},
{
"text": "Это незаконно!",
"next": 11
}
]
},
{
"id": 11,
"text": "(истерично смеётся) Законно? Ха! В этом городе? (внезапно серьёзнеет) Ладно, иди отсюда, мальчик. Ищи себе 'законную' работу. (поворачивается спиной)",
"answers": [
{
"end": true
}
]
},
{
"id": "hack_job",
"type": "quest",
"title": "Взлом в подвале",
"description": "Нужно проникнуть в подвал, сказав охраннику пароль 'берёзовый сок', и взломать данные",
"location": "/locations/basement.json",
"reward": "500 кредитов",
"next": 8
}
]
}

View File

@@ -0,0 +1,33 @@
{
"name": "Охранник",
"avatar": "/images/npc/guard.jpg",
"filename": "Oxranik.json",
"dialog": [
{
"id": 0,
"text": "Стоять! Кто такой?",
"answers": [
{
"text": "Березовый сок",
"next": 1,
"required_quest": "hack_job"
},
{
"text": "Я ошибся дверью",
"end": true
}
]
},
{
"id": 1,
"text": "(кивает) Проходи. Но предупреждаю - если что-то пойдет не так, я тебя не знал.",
"answers": [
{
"text": "Понял",
"quest_progress": "hack_job",
"end": true
}
]
}
]
}

View File

@@ -0,0 +1,45 @@
{
"name": "Серега Пират",
"avatar": "/images/npc/bartender.jpg",
"filename": "bartender.json",
"dialog": [
{
"id": 0,
"text": "Ну что, дружок, застрял как муха в паутине? Или просто решил проверить, насколько крепки эти стены?",
"answers": [
{
"text": "Я... кажется, ошибся дверью.",
"end": true
},
{
"text": "Мне сказали, тут можно «устроиться». От Галины.",
"next": 2
}
]
},
{
"id": 2,
"text": "Ага, Галка-весточка. Слушай сюда: правила простые — садишься за комп, выполняешь два задания. Первое — по звукам угадаешь пароль. Второе — в финансовых документах найдёшь косяк. Справишься — получишь свои кровные. Не справишься... ну, сам понимаешь.",
"answers": [
{
"text": "И где этот ваш комп?",
"next": 3
},
{
"text": "А сложно будет?",
"next": 3
}
]
},
{
"id": 3,
"text": "*кивает на потрёпанный системник в углу* Там всё включено. Разберёшься. Главное — уши навостри и глаза пошире открой. Как будешь готов — жми любую кнопку.",
"answers": [
{
"text": "*Подойти к компьютеру*",
"end": true
}
]
}
]
}

105
public/dialogs/guard.json Normal file
View File

@@ -0,0 +1,105 @@
{
"name": "Саша Белый",
"avatar": "/images/npc/guard.jpg",
"filename": "guard.json",
"dialog": [
{
"id": 0,
"text": "А вот и новенький подъехал… Чё, глаза такие круглые? Добро пожаловать в Realternity Moscow City, братан. Тут не экскурсия, так что уши на макушке держи.",
"answers": [
{
"text": "Понял...",
"next": 1
},
{
"text": "Что это за место?",
"next": 1
}
]
},
{
"id": 1,
"text": "Город у нас большой, светится неоном, как новогодняя ёлка, но под этой мишурой — помойка, крысы да волки. Вверх глянешь — небоскрёбы корпораций, вниз — подземка, где людей за карточку еды режут. Каждый тут сам за себя.",
"answers": [
{
"text": "И что делать?",
"next": 2
},
{
"text": "Звучит жутко...",
"next": 2
}
]
},
{
"id": 2,
"text": "Первое правило — хочешь жить, ищи работу. Деньги — это воздух. Без них ты тут не человек, а мусор под ногами.",
"answers": [
{
"text": "А если работы нет?",
"next": 3
},
{
"text": "Где искать?",
"next": 4
}
]
},
{
"id": 3,
"text": "Нет работы? Ну, значит, ищешь... альтернативы. Тут таких путей — как тараканов в общаге. Главное — не ной, действуй.",
"answers": [
{
"text": "Какие альтернативы?",
"next": 5
},
{
"text": "Понятно...",
"next": 6
}
]
},
{
"id": 4,
"text": "Короче, если совсем не врубаешься, топай в Центр Трудоустройства «Нижний Эшелон». Там помогут... ну, если не кинут. Смотри в оба, братан.",
"answers": [
{
"text": "Спасибо за совет",
"next": 6
},
{
"text": "А где это?",
"next": 6
}
]
},
{
"id": 5,
"text": "Честный ты, нечестный — неважно. Главное — чтоб ты выбрал, по какой дорожке топать. По свету, по тени… или, может, будешь тем, кто идёт посередине и стреляет в обе стороны. Тут за всё платят — вопрос только, чем.",
"answers": [
{
"text": "Ясно...",
"next": 6
},
{
"text": "Страшновато",
"next": 6
}
]
},
{
"id": 6,
"text": "И запомни: здесь не детский сад. Никто за ручку водить не будет. Или ты адаптируешься… или сгниёшь на подворотне. Всё просто.",
"answers": [
{
"text": "Понял, спасибо",
"end": true
},
{
"text": "До встречи",
"end": true
}
]
}
]
}

81
public/docker-compose.yml Normal file
View File

@@ -0,0 +1,81 @@
version: "3.8"
services:
cockroachdb:
image: cockroachdb/cockroach:latest-v23.1
command: start-single-node --insecure --store=attrs=ssd,path=/var/lib/cockroach/
restart: "no"
volumes:
- data:/var/lib/cockroach
expose:
- "8080"
- "26257"
ports:
- "26257:26257"
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"]
interval: 3s
timeout: 3s
retries: 5
nakama:
image: registry.heroiclabs.com/heroiclabs/nakama:3.22.0
entrypoint:
- "/bin/sh"
- "-ecx"
- >
/nakama/nakama migrate up --database.address root@cockroachdb:26257 &&
exec /nakama/nakama --name nakama1 --database.address root@cockroachdb:26257 --logger.level DEBUG --session.token_expiry_sec 7200 --metrics.prometheus_port 9100
restart: "no"
links:
- "cockroachdb:db"
depends_on:
cockroachdb:
condition: service_healthy
prometheus:
condition: service_started
environment:
- socket.server_key=mySuperSecretKey123
volumes:
- ./:/nakama/data
expose:
- "7349"
- "7350"
- "7351"
- "9100"
ports:
- "7349:7349"
- "7350:7350"
- "7351:7351"
healthcheck:
test: ["CMD", "/nakama/nakama", "healthcheck"]
interval: 10s
timeout: 5s
retries: 5
prometheus:
image: prom/prometheus
entrypoint: /bin/sh -c
command: |
'sh -s <<EOF
cat > ./prometheus.yml <<EON
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ['localhost:9090']
- job_name: nakama
metrics_path: /
static_configs:
- targets: ['nakama:9100']
EON
prometheus --config.file=./prometheus.yml
EOF'
ports:
- "9090:9090"
environment:
- socket.server_key=SecretKey123321fs
volumes:
data:

BIN
public/images/222.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
public/images/npc/guard.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

17
public/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>Multiplayer Three.js Game on React</title>
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
</head>
<body style="margin:0; overflow:hidden;">
<noscript>Для работы приложения требуется включить JavaScript.</noscript>
<div id="root"></div>
</body>
</html>

1
public/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

BIN
public/models/Punk.glb Normal file

Binary file not shown.

Binary file not shown.

BIN
public/models/Xbot.glb Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,155 @@
{
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": 406,
"type": "VEC2"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 406,
"type": "VEC3"
},
{
"bufferView": 2,
"componentType": 5126,
"count": 406,
"type": "VEC4"
},
{
"bufferView": 3,
"componentType": 5126,
"count": 406,
"type": "VEC3",
"max": [
0.02128091,
0.06284806,
0.0138090011
],
"min": [
-0.02128091,
-4.773855E-05,
-0.013809
]
},
{
"bufferView": 4,
"componentType": 5123,
"count": 2046,
"type": "SCALAR"
}
],
"asset": {
"generator": "glTF Tools for Unity",
"version": "2.0"
},
"bufferViews": [
{
"buffer": 0,
"byteLength": 3248
},
{
"buffer": 0,
"byteOffset": 3248,
"byteLength": 4872
},
{
"buffer": 0,
"byteOffset": 8120,
"byteLength": 6496
},
{
"buffer": 0,
"byteOffset": 14616,
"byteLength": 4872
},
{
"buffer": 0,
"byteOffset": 19488,
"byteLength": 4092
}
],
"buffers": [
{
"uri": "Avocado.bin",
"byteLength": 23580
}
],
"images": [
{
"uri": "Avocado_baseColor.png"
},
{
"uri": "Avocado_roughnessMetallic.png"
},
{
"uri": "Avocado_normal.png"
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"TEXCOORD_0": 0,
"NORMAL": 1,
"TANGENT": 2,
"POSITION": 3
},
"indices": 4,
"material": 0
}
],
"name": "Avocado"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0
},
"metallicRoughnessTexture": {
"index": 1
}
},
"normalTexture": {
"index": 2
},
"name": "2256_Avocado_d"
}
],
"nodes": [
{
"mesh": 0,
"rotation": [
0.0,
1.0,
0.0,
0.0
],
"name": "Avocado"
}
],
"scene": 0,
"scenes": [
{
"nodes": [
0
]
}
],
"textures": [
{
"source": 0
},
{
"source": 1
},
{
"source": 2
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
public/models/character.glb Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More