Первый коммит после распаковки архива
3
.env
Normal 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
@@ -0,0 +1,5 @@
|
||||
# зависимости npm
|
||||
node_modules/
|
||||
|
||||
# скомпилированные файлы
|
||||
build/
|
||||
145
README.md
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
14
on NewAugust with updated local version
Normal 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 [32m++[m[31m--[m
|
||||
db.js | 4 [32m+[m[31m---[m
|
||||
db1.js | 2 [32m+[m[31m-[m
|
||||
package-lock.json | 57 [32m+++++++++++++++++++++++++++++[m[31m--------------------------[m
|
||||
server.js | 10 [32m++++[m[31m------[m
|
||||
src/Game.js | 40 [32m+++++++++++++[m[31m-------------------------[m
|
||||
6 files changed, 51 insertions(+), 66 deletions(-)
|
||||
31030
package-lock.json
generated
Normal file
46
package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
public/animations/feminine/glb/idle/F_Standing_Idle_001.glb
Normal file
BIN
public/animations/feminine/glb/locomotion/F_Walk_001.glb
Normal file
BIN
public/animations/feminine/glb/locomotion/F_Walk_002.glb
Normal file
BIN
public/animations/masculine/glb/idle/M_Standing_Idle_001.glb
Normal file
BIN
public/animations/masculine/glb/idle/M_Standing_Idle_002.glb
Normal file
BIN
public/animations/masculine/glb/locomotion/M_Walk_001.fbx
Normal file
BIN
public/animations/masculine/glb/locomotion/M_Walk_001.glb
Normal file
BIN
public/animations/masculine/glb/locomotion/M_Walk_002.fbx
Normal file
BIN
public/animations/masculine/glb/locomotion/M_Walk_002.glb
Normal file
BIN
public/audio/TR4-FG8-Hj2.ogg
Normal file
BIN
public/audio/X-b7kG-z3Lp.ogg
Normal file
BIN
public/audio/firs.ogg
Normal file
153
public/dialogs/Adventurer.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
167
public/dialogs/BeachCharacter.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
33
public/dialogs/Oxranik.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
45
public/dialogs/bartender.json
Normal 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
@@ -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
@@ -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
|
After Width: | Height: | Size: 77 KiB |
BIN
public/images/npc/bartender.jpg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
public/images/npc/guard.jpg
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
public/images/photo_2025-02-22_19-35-03.jpg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
public/images/photo_2025-02-22_19-39-34.jpg
Normal file
|
After Width: | Height: | Size: 255 KiB |
17
public/index.html
Normal 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
BIN
public/models/Large Building (1).glb
Normal file
BIN
public/models/Punk.glb
Normal file
BIN
public/models/Skyscraper.glb
Normal file
BIN
public/models/Xbot.glb
Normal file
BIN
public/models/animations/M_Walk_001.glb
Normal file
BIN
public/models/animations/M_Walk_002.glb
Normal file
BIN
public/models/animations/Walking.glb
Normal file
BIN
public/models/avocado/Avocado.bin
Normal file
155
public/models/avocado/Avocado.gltf
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
public/models/avocado/Avocado_baseColor.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
public/models/avocado/Avocado_normal.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
public/models/avocado/Avocado_roughnessMetallic.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |