added inventory manager and .env for the client

This commit is contained in:
MaksSlyzar 2026-04-01 00:23:42 +03:00
parent 7302a54cd7
commit 42ae328b56
8 changed files with 223 additions and 149 deletions

View File

@ -4,7 +4,7 @@ import axios from "axios";
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const API_URL = "http://localhost:3000/api/auth";
const API_URL = `${import.meta.env.API_URL}/api/auth`;
const [user, setUser] = useState(() => {
const savedUser = localStorage.getItem("user");

View File

@ -3,6 +3,7 @@ import { useSocket } from "../../../hooks/useSocket";
import GameDataManager from "../../../services/GameDataManager.js";
import Button from "../../../components/ui/Button";
import "./styles/InventoryTab.css";
import { getServerUrl } from "../../../config/api.js";
const InventoryTab = () => {
const { socket } = useSocket();
@ -10,19 +11,24 @@ const InventoryTab = () => {
const [equipment, setEquipment] = useState({});
const [selectedItem, setSelectedItem] = useState(null);
const [showModal, setShowModal] = useState(false);
const CONNECT_URL = getServerUrl();
const ASSET_BASE_URL = `${CONNECT_URL}/static/`;
const ASSET_BASE_URL = "http://localhost:5003/static/";
const slots = [
"helmet",
"armor",
"weapon",
"pants",
"boots",
"hands",
"accessory",
"shipHull",
];
const equipmentSlots = {
personal: [
{ id: "personal_helmet", label: "HELMET" },
{ id: "personal_suit", label: "SUIT" },
{ id: "personal_gloves", label: "GLOVES" },
{ id: "personal_boots", label: "BOOTS" },
{ id: "personal_backpack", label: "BACKPACK" },
{ id: "personal_weapons", label: "WEAPON" },
],
ship: [
{ id: "ship_hull", label: "HULL" },
{ id: "ship_shields", label: "SHIELDS" },
{ id: "ship_engines", label: "ENGINES" },
],
};
const getFullTextureUrl = (path) => {
if (!path) return "/assets/no-image.png";
@ -95,6 +101,36 @@ const InventoryTab = () => {
socket.emit("player:unequip_item", { slot });
};
const renderSlotGroup = (title, groupSlots) => (
<div className="equip-group">
<div className="group-label">{title}</div>
<div className="equip-list-compact">
{groupSlots.map((slot) => (
<div
key={slot.id}
className={`equip-row-mini ${equipment[slot.id] ? "occupied" : ""}`}
onClick={() => equipment[slot.id] && unequipItem(slot.id)}
>
<span className="slot-name-tiny">{slot.label}</span>
<div
className={`equip-box-mini ${equipment[slot.id]?.rarity || ""}`}
>
{equipment[slot.id] ? (
<img
src={equipment[slot.id].textureUrl}
alt={slot.id}
className="item-img-mini"
/>
) : (
<span className="plus">+</span>
)}
</div>
</div>
))}
</div>
</div>
);
return (
<div className="inv-adaptive-container">
<div className="inv-header-compact">
@ -109,30 +145,9 @@ const InventoryTab = () => {
<div className="inv-layout-wrapper">
<section className="inv-panel loadout">
<div className="panel-label">CORE_SYSTEMS</div>
<div className="equip-list-compact">
{slots.map((slot) => (
<div
key={slot}
className={`equip-row-mini ${equipment[slot] ? "occupied" : ""}`}
onClick={() => equipment[slot] && unequipItem(slot)}
>
<span className="slot-name-tiny">{slot.toUpperCase()}</span>
<div
className={`equip-box-mini ${equipment[slot]?.rarity || ""}`}
>
{equipment[slot] ? (
<img
src={equipment[slot].textureUrl}
alt={slot}
className="item-img-mini"
/>
) : (
<span className="plus">+</span>
)}
</div>
</div>
))}
</div>
{renderSlotGroup("PERSON", equipmentSlots.personal)}
{renderSlotGroup("SHIP", equipmentSlots.ship)}
</section>
<section className="inv-panel cargo">
@ -179,7 +194,8 @@ const InventoryTab = () => {
Object.entries(selectedItem.stats).map(([k, v]) => (
<div key={k} className="stat-line">
<span className="label">
{GameDataManager.getStatName(k).toUpperCase()}
{GameDataManager.getStatName?.(k)?.toUpperCase() ||
k.toUpperCase()}
</span>
<span className="value">+{v}</span>
</div>

View File

@ -1,39 +1,72 @@
import React from 'react';
import './ItemModal.css';
import React from "react";
import "./ItemModal.css";
const ItemModal = ({ item, onClose, onEquip, getStatIcon, formatStatName }) => {
const ItemModal = ({
item,
onClose,
onEquip,
onUnequip,
isEquipped,
getStatIcon,
formatStatName,
}) => {
if (!item) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>&times;</button>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>
&times;
</button>
<div className="details-view">
<div className="details-header">
<h4 className={`item-name ${item.rarity}`}>{item.name}</h4>
<h4 className={`item-name ${item.rarity}`}>
{item.displayName || item.name}
</h4>
<span className={`rarity-badge ${item.rarity}`}>{item.rarity}</span>
</div>
<p className="item-description">{item.description}</p>
<div className="item-stats-container">
<div className="stat-row quality-row">
<span className="stat-label"><i className="fas fa-medal"></i> Quality</span>
<span className={`stat-value ${item.rarity}`}>{item.rarity.toUpperCase()}</span>
</div>
{item.stats && <div className="stat-divider"></div>}
{item.stats && Object.entries(item.stats).map(([statName, value]) => (
<div key={statName} className="stat-row">
<span className="stat-label"><i className={getStatIcon(statName)}></i> {formatStatName(statName)}</span>
<span className="stat-value">+{value}</span>
</div>
))}
{item.stats &&
Object.entries(item.stats).map(([statName, value]) => (
<div key={statName} className="stat-row">
<span className="stat-label">
<i className={getStatIcon?.(statName)}></i>{" "}
{formatStatName
? formatStatName(statName)
: statName.toUpperCase()}
</span>
<span className="stat-value">+{value}</span>
</div>
))}
</div>
<button className="btn-equip" onClick={() => { onEquip(item); onClose(); }}>
Equip System
</button>
{isEquipped ? (
<button
className="btn-equip unequip"
onClick={() => {
onUnequip(item.currentSlot);
onClose();
}}
>
DISCONNECT_SYSTEM
</button>
) : (
item.canEquip && (
<button
className="btn-equip"
onClick={() => {
onEquip(item);
onClose();
}}
>
INITIALIZE_EQUIP
</button>
)
)}
</div>
</div>
</div>

View File

@ -7,18 +7,17 @@ const ServerSection = ({ onBack, onSelect }) => {
const [servers, setServers] = useState([]);
const [loading, setLoading] = useState(false);
const [joiningId, setJoiningId] = useState(null);
const [searchTerm, setSearchTerm] = useState(""); // Стан для пошуку
const [searchTerm, setSearchTerm] = useState("");
const fetchServers = async () => {
setLoading(true);
const startTime = Date.now();
try {
const response = await axios.get(
"http://localhost:3000/api/servers/list",
);
const API_URL = import.meta.env.API_URL;
const response = await axios.get(`${API_URL}/api/servers/list`);
const elapsedTime = Date.now() - startTime;
const remainingTime = Math.max(0, 2000 - elapsedTime); // Трохи зменшив штучну затримку
const remainingTime = Math.max(0, 2000 - elapsedTime);
await new Promise((resolve) => setTimeout(resolve, remainingTime));
setServers(response.data);
@ -59,7 +58,6 @@ const ServerSection = ({ onBack, onSelect }) => {
fetchServers();
}, []);
// Фільтрація серверів на основі searchTerm
const filteredServers = servers.filter((server) =>
searchTerm.length > 0
? server.name?.toLowerCase().includes(searchTerm.toLowerCase())

View File

@ -0,0 +1,59 @@
const { Player, Inventory } = require("../models");
const DatapackLoader = require("./DatapackLoader");
class InventoryManager {
async getInventory(playerId) {
return await Inventory.findAll({
where: { playerId },
attributes: ["itemId", "quantity"],
});
}
async getEquipment(playerId) {
const player = await Player.findByPk(playerId);
if (!player) return {};
const slots = [
"personal_helmet",
"personal_suit",
"personal_gloves",
"personal_backpack",
"personal_boots",
"personal_weapons",
"ship_hull",
"ship_shields",
"ship_engines",
];
const equipment = {};
slots.forEach((slot) => {
equipment[slot] = player[`equipped_${slot}`] || null;
});
return equipment;
}
async equipItem(playerId, itemId, slot) {
const hasItem = await Inventory.findOne({ where: { playerId, itemId } });
if (!hasItem) throw new Error("ITEM_NOT_FOUND");
const itemInfo = DatapackLoader.getItem(itemId);
if (!itemInfo) throw new Error("INVALID_ITEM_DATA");
const allowedSlot = itemInfo.meta?.equipmentSlot;
if (allowedSlot !== slot) {
throw new Error("INVALID_SLOT_FOR_ITEM");
}
const dbField = `equipped_${slot}`.replace("original:", "");
await Player.update({ [dbField]: itemId }, { where: { id: playerId } });
return itemInfo;
}
async unequipItem(playerId, slot) {
const dbField = `equipped_${slot}`;
await Player.update({ [dbField]: null }, { where: { id: playerId } });
return true;
}
}
module.exports = new InventoryManager();

View File

@ -16,19 +16,31 @@ class SessionManager {
sceneData: null,
joinedAt: Date.now(),
equipment: {
weapon: playerRaw.equippedWeapon,
armor: playerRaw.equippedArmor,
engine: playerRaw.equippedEngine,
shield: playerRaw.equippedShield,
helmet: playerRaw.equippedHelmet,
boots: playerRaw.equippedBoots,
hands: playerRaw.equippedHands,
pants: playerRaw.equippedPants,
accessory: playerRaw.equippedAccessory,
personal_helmet: playerRaw.equipped_personal_helmet,
personal_suit: playerRaw.equipped_personal_suit,
personal_gloves: playerRaw.equipped_personal_gloves,
personal_backpack: playerRaw.equipped_personal_backpack,
personal_boots: playerRaw.equipped_personal_boots,
personal_weapons: playerRaw.equipped_personal_weapons,
ship_hull: playerRaw.equipped_ship_hull,
ship_shields: playerRaw.equipped_ship_shields,
ship_engines: playerRaw.equipped_ship_engines,
ship_weapon_1: playerRaw.equipped_ship_weapon_1,
ship_weapon_2: playerRaw.equipped_ship_weapon_2,
},
});
}
updateEquipment(socketId, slot, itemId) {
const session = this.sessions.get(socketId);
if (session && session.equipment.hasOwnProperty(slot)) {
session.equipment[slot] = itemId;
} else if (session) {
session.equipment[slot] = itemId;
}
}
setPlayerScene(socketId, sceneName, data = null) {
const session = this.sessions.get(socketId);
if (session) {
@ -43,13 +55,6 @@ class SessionManager {
return this.sessions.get(socketId);
}
updateEquipment(socketId, slot, itemId) {
const session = this.sessions.get(socketId);
if (session) {
session.equipment[slot] = itemId;
}
}
updateStatus(socketId, newStatus, location = null) {
const session = this.sessions.get(socketId);
if (session) {

View File

@ -42,14 +42,22 @@ const Player = sequelize.define("Player", {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
equippedWeapon: { type: DataTypes.STRING },
equippedArmor: { type: DataTypes.STRING },
equippedEngine: { type: DataTypes.STRING },
equippedAccessory: { type: DataTypes.STRING },
equippedHelmet: { type: DataTypes.STRING },
equippedBoots: { type: DataTypes.STRING },
equippedHands: { type: DataTypes.STRING },
equippedPants: { type: DataTypes.STRING },
equipped_personal_helmet: { type: DataTypes.STRING },
equipped_personal_suit: { type: DataTypes.STRING },
equipped_personal_gloves: { type: DataTypes.STRING },
equipped_personal_backpack: { type: DataTypes.STRING },
equipped_personal_boots: { type: DataTypes.STRING },
equipped_personal_weapons: { type: DataTypes.STRING },
equipped_personal_accessory_1: { type: DataTypes.STRING },
equipped_personal_accessory_2: { type: DataTypes.STRING },
equipped_ship_hull: { type: DataTypes.STRING },
equipped_ship_shields: { type: DataTypes.STRING },
equipped_ship_engines: { type: DataTypes.STRING },
equipped_ship_weapon_1: { type: DataTypes.STRING },
equipped_ship_weapon_2: { type: DataTypes.STRING },
});
module.exports = Player;

View File

@ -1,82 +1,35 @@
const { Player, Inventory } = require("../../models");
const inventoryManager = require("../../game/InventoryManager");
const sessionManager = require("../../game/SessionManager");
module.exports = (io, socket) => {
const userId = socket.user?.id;
if (!userId) return;
socket.on("player:get_inventory", async () => {
try {
const userItems = await Inventory.findAll({
where: { playerId: userId },
attributes: ["itemId", "quantity"],
});
socket.emit("player:inventory_data", userItems);
const items = await inventoryManager.getInventory(userId);
socket.emit("player:inventory_data", items);
} catch (err) {
console.error("Inventory Fetch Error:", err.message);
socket.emit("error", { message: "Failed to load inventory" });
socket.emit("error", { message: "LOAD_INVENTORY_FAILED" });
}
});
socket.on("player:get_equipment", async () => {
try {
const player = await Player.findByPk(userId);
if (!player) return;
const slots = [
"Weapon",
"Armor",
"Helmet",
"Boots",
"Hands",
"Pants",
"Engine",
"Accessory",
];
const equipment = {};
slots.forEach((slot) => {
const itemId = player[`equipped${slot}`];
equipment[slot.toLowerCase()] = itemId || null;
});
const equipment = await inventoryManager.getEquipment(userId);
socket.emit("player:equipment_data", equipment);
} catch (err) {
console.error("Equipment Fetch Error:", err.message);
console.error(err);
}
});
socket.on("player:equip_item", async ({ itemId, slot }) => {
try {
const hasItem = await Inventory.findOne({
where: { playerId: userId, itemId: itemId },
});
if (!hasItem) {
return socket.emit("error", { message: "You don't own this item" });
}
const DatapackLoader = require("../../game/DatapackLoader");
const itemInfo = DatapackLoader.getItem(itemId);
if (!itemInfo) return;
if (slot === "weapon" && itemInfo.type !== "weapon")
return socket.emit("error", { message: "Not a weapon" });
if (
["helmet", "armor", "boots", "pants", "hands"].includes(slot) &&
itemInfo.type !== "armour"
) {
return socket.emit("error", { message: "Not armor" });
}
const slotField = `equipped${slot.charAt(0).toUpperCase() + slot.slice(1)}`;
await Player.update({ [slotField]: itemId }, { where: { id: userId } });
const itemInfo = await inventoryManager.equipItem(userId, itemId, slot);
sessionManager.updateEquipment(socket.id, slot, itemId);
socket.emit("player:item_equipped", { slot, itemId: itemId });
socket.emit("player:item_equipped", { slot, itemId });
socket.broadcast.emit("player:visible_changed", {
playerId: userId,
@ -84,21 +37,23 @@ module.exports = (io, socket) => {
texturePath: itemInfo.texture,
});
} catch (err) {
console.error("Equip Error:", err.message);
socket.emit("error", { message: "Equip failed" });
socket.emit("error", { message: err.message });
}
});
socket.on("player:unequip_item", async ({ slot }) => {
try {
const slotField = `equipped${slot.charAt(0).toUpperCase() + slot.slice(1)}`;
await Player.update({ [slotField]: null }, { where: { id: userId } });
await inventoryManager.unequipItem(userId, slot);
sessionManager.updateEquipment(socket.id, slot, null);
socket.emit("player:item_unequipped", { slot });
socket.broadcast.emit("player:visible_changed", {
playerId: userId,
slot,
texturePath: null,
});
} catch (err) {
console.error("Unequip Error:", err.message);
console.error(err);
}
});
};