504 lines
16 KiB
JavaScript
504 lines
16 KiB
JavaScript
/**
|
|
* NPCShopSystem.js
|
|
* ================
|
|
* KRVAVA ŽETEV - NPC Trading & Shop System (Phase 38)
|
|
*
|
|
* Features:
|
|
* - 4 NPC shop types (Blacksmith, Baker, Trader, Healer)
|
|
* - Shop UI with buy/sell
|
|
* - Dynamic pricing
|
|
* - Stock management
|
|
* - Relationship discounts
|
|
*
|
|
* @author NovaFarma Team
|
|
* @date 2025-12-23
|
|
*/
|
|
|
|
class NPCShopSystem {
|
|
constructor(scene) {
|
|
this.scene = scene;
|
|
|
|
// Shop registry
|
|
this.shops = new Map();
|
|
this.currentShop = null;
|
|
|
|
// Shop UI
|
|
this.shopContainer = null;
|
|
this.isShopOpen = false;
|
|
|
|
// Player inventory reference
|
|
this.playerInventory = null;
|
|
this.playerZlatniki = 0;
|
|
|
|
console.log('🛒 NPCShopSystem initialized');
|
|
|
|
// Register all shops
|
|
this.registerShops();
|
|
|
|
// Create shop UI
|
|
this.createShopUI();
|
|
}
|
|
|
|
/**
|
|
* Register all NPC shops
|
|
*/
|
|
registerShops() {
|
|
const shops = [
|
|
{
|
|
id: 'blacksmith',
|
|
name: 'Kovač (Blacksmith)',
|
|
npc: 'Ivan the Blacksmith',
|
|
icon: '⚒️',
|
|
location: { x: 200, y: 200 },
|
|
inventory: [
|
|
// Tools
|
|
{ id: 'iron_axe', name: 'Iron Axe', price: 200, stock: 5, category: 'tools', locked: true },
|
|
{ id: 'iron_pickaxe', name: 'Iron Pickaxe', price: 200, stock: 5, category: 'tools', locked: true },
|
|
{ id: 'iron_hoe', name: 'Iron Hoe', price: 150, stock: 5, category: 'tools', locked: true },
|
|
{ id: 'watering_can', name: 'Watering Can', price: 100, stock: 10, category: 'tools' },
|
|
|
|
// Weapons
|
|
{ id: 'iron_sword', name: 'Iron Sword', price: 500, stock: 3, category: 'weapons', locked: true },
|
|
{ id: 'steel_sword', name: 'Steel Sword', price: 1000, stock: 2, category: 'weapons', locked: true },
|
|
{ id: 'crossbow', name: 'Crossbow', price: 800, stock: 2, category: 'weapons', locked: true },
|
|
|
|
|
|
// Armor
|
|
{ id: 'leather_armor', name: 'Leather Armor', price: 300, stock: 5, category: 'armor' },
|
|
{ id: 'iron_armor', name: 'Iron Armor', price: 800, stock: 3, category: 'armor' }
|
|
],
|
|
buyback: ['iron_ore', 'steel_bar', 'scrap_metal']
|
|
},
|
|
{
|
|
id: 'baker',
|
|
name: 'Pekarica (Baker)',
|
|
npc: 'Maria the Baker',
|
|
icon: '🍞',
|
|
location: { x: 250, y: 200 },
|
|
inventory: [
|
|
// Food
|
|
{ id: 'bread', name: 'Bread', price: 10, stock: 50, category: 'food' },
|
|
{ id: 'cheese', name: 'Cheese', price: 20, stock: 30, category: 'food' },
|
|
{ id: 'apple_pie', name: 'Apple Pie', price: 50, stock: 20, category: 'food' },
|
|
{ id: 'cake', name: 'Cake', price: 100, stock: 10, category: 'food' },
|
|
|
|
// Recipes
|
|
{ id: 'recipe_cookies', name: 'Cookie Recipe', price: 200, stock: 1, category: 'recipes' },
|
|
{ id: 'recipe_pizza', name: 'Pizza Recipe', price: 300, stock: 1, category: 'recipes' },
|
|
|
|
// Ingredients
|
|
{ id: 'flour', name: 'Flour', price: 15, stock: 100, category: 'ingredients' },
|
|
{ id: 'sugar', name: 'Sugar', price: 20, stock: 80, category: 'ingredients' },
|
|
{ id: 'yeast', name: 'Yeast', price: 10, stock: 50, category: 'ingredients' }
|
|
],
|
|
buyback: ['wheat', 'milk', 'eggs', 'berries']
|
|
},
|
|
{
|
|
id: 'trader',
|
|
name: 'Trgovec (General Trader)',
|
|
npc: 'Gregor the Trader',
|
|
icon: '💰',
|
|
location: { x: 300, y: 200 },
|
|
inventory: [
|
|
// Seeds
|
|
{ id: 'wheat_seeds', name: 'Wheat Seeds', price: 5, stock: 200, category: 'seeds' },
|
|
{ id: 'corn_seeds', name: 'Corn Seeds', price: 8, stock: 150, category: 'seeds' },
|
|
{ id: 'tomato_seeds', name: 'Tomato Seeds', price: 10, stock: 100, category: 'seeds' },
|
|
{ id: 'strawberry_seeds', name: 'Strawberry Seeds', price: 15, stock: 80, category: 'seeds' },
|
|
{ id: 'cannabis_seeds', name: 'Cannabis Seeds', price: 20, stock: 50, category: 'seeds' },
|
|
|
|
// Materials
|
|
{ id: 'wood', name: 'Wood', price: 10, stock: 500, category: 'materials' },
|
|
{ id: 'stone', name: 'Stone', price: 15, stock: 300, category: 'materials' },
|
|
{ id: 'clay', name: 'Clay', price: 20, stock: 200, category: 'materials' },
|
|
|
|
// Special
|
|
{ id: 'saddle', name: 'Saddle', price: 500, stock: 2, category: 'special' },
|
|
{ id: 'bouquet', name: 'Bouquet', price: 100, stock: 10, category: 'special' },
|
|
{ id: 'mermaid_pendant', name: 'Mermaid Pendant', price: 5000, stock: 1, category: 'special' }
|
|
],
|
|
buyback: ['crops', 'foraged_items', 'fish']
|
|
},
|
|
{
|
|
id: 'healer',
|
|
name: 'Zdravnik (Healer)',
|
|
npc: 'Dr. Ana Kovač',
|
|
icon: '⚕️',
|
|
location: { x: 350, y: 200 },
|
|
inventory: [
|
|
// Potions
|
|
{ id: 'health_potion', name: 'Health Potion', price: 50, stock: 50, category: 'potions' },
|
|
{ id: 'stamina_potion', name: 'Stamina Potion', price: 40, stock: 50, category: 'potions' },
|
|
{ id: 'antidote', name: 'Antidote', price: 30, stock: 30, category: 'potions' },
|
|
{ id: 'cure_infection', name: 'Cure Infection', price: 200, stock: 10, category: 'potions' },
|
|
|
|
// Research
|
|
{ id: 'cure_research_1', name: 'Cure Research Notes I', price: 1000, stock: 1, category: 'research' },
|
|
{ id: 'cure_research_2', name: 'Cure Research Notes II', price: 2000, stock: 1, category: 'research' },
|
|
|
|
// Medical supplies
|
|
{ id: 'bandage', name: 'Bandage', price: 15, stock: 100, category: 'medical' },
|
|
{ id: 'medicine', name: 'Medicine', price: 80, stock: 30, category: 'medical' }
|
|
],
|
|
buyback: ['herbs', 'mushrooms', 'zombie_samples', 'cannabis', 'cannabis_buds']
|
|
}
|
|
];
|
|
|
|
shops.forEach(shop => this.shops.set(shop.id, shop));
|
|
|
|
console.log(`✅ Registered ${this.shops.size} NPC shops`);
|
|
}
|
|
|
|
/**
|
|
* Create shop UI
|
|
*/
|
|
createShopUI() {
|
|
const width = this.scene.cameras.main.width;
|
|
const height = this.scene.cameras.main.height;
|
|
|
|
// Main container
|
|
this.shopContainer = this.scene.add.container(width / 2, height / 2);
|
|
this.shopContainer.setScrollFactor(0);
|
|
this.shopContainer.setDepth(10000);
|
|
this.shopContainer.setVisible(false);
|
|
|
|
// Background
|
|
const bg = this.scene.add.rectangle(0, 0, 900, 600, 0x1a1a1a, 0.95);
|
|
bg.setStrokeStyle(3, 0xFFD700);
|
|
this.shopContainer.add(bg);
|
|
|
|
// Title (will be updated)
|
|
this.shopTitle = this.scene.add.text(0, -280, '🛒 SHOP', {
|
|
fontSize: '32px',
|
|
fontFamily: 'Arial',
|
|
color: '#FFD700',
|
|
fontStyle: 'bold'
|
|
});
|
|
this.shopTitle.setOrigin(0.5);
|
|
this.shopContainer.add(this.shopTitle);
|
|
|
|
// Close button
|
|
const closeBtn = this.scene.add.text(430, -280, '❌', {
|
|
fontSize: '24px',
|
|
cursor: 'pointer'
|
|
});
|
|
closeBtn.setInteractive();
|
|
closeBtn.on('pointerdown', () => this.closeShop());
|
|
this.shopContainer.add(closeBtn);
|
|
|
|
// Player money display
|
|
this.moneyText = this.scene.add.text(-430, -250, '💰 0 Zlatniki', {
|
|
fontSize: '18px',
|
|
fontFamily: 'Arial',
|
|
color: '#FFD700'
|
|
});
|
|
this.shopContainer.add(this.moneyText);
|
|
|
|
// Category tabs
|
|
this.createCategoryTabs();
|
|
|
|
// Item list container
|
|
this.itemListContainer = this.scene.add.container(0, 0);
|
|
this.shopContainer.add(this.itemListContainer);
|
|
|
|
console.log('✅ Shop UI created');
|
|
}
|
|
|
|
/**
|
|
* Create category tabs
|
|
*/
|
|
createCategoryTabs() {
|
|
const categories = ['all', 'tools', 'weapons', 'food', 'seeds', 'potions'];
|
|
const tabWidth = 120;
|
|
const startX = -400;
|
|
const y = -200;
|
|
|
|
categories.forEach((category, index) => {
|
|
const tab = this.scene.add.rectangle(
|
|
startX + (index * tabWidth),
|
|
y,
|
|
110, 40,
|
|
0x2d2d2d
|
|
);
|
|
tab.setStrokeStyle(2, 0x666666);
|
|
tab.setInteractive();
|
|
tab.on('pointerdown', () => this.filterByCategory(category));
|
|
|
|
const label = this.scene.add.text(
|
|
startX + (index * tabWidth),
|
|
y,
|
|
category.toUpperCase(),
|
|
{
|
|
fontSize: '14px',
|
|
fontFamily: 'Arial',
|
|
color: '#ffffff'
|
|
}
|
|
);
|
|
label.setOrigin(0.5);
|
|
|
|
this.shopContainer.add(tab);
|
|
this.shopContainer.add(label);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Open shop
|
|
*/
|
|
openShop(shopId) {
|
|
const shop = this.shops.get(shopId);
|
|
if (!shop) {
|
|
console.error(`Shop ${shopId} not found!`);
|
|
return false;
|
|
}
|
|
|
|
this.currentShop = shop;
|
|
this.isShopOpen = true;
|
|
|
|
// Update title
|
|
this.shopTitle.setText(`${shop.icon} ${shop.name}`);
|
|
|
|
// Update money
|
|
this.updateMoneyDisplay();
|
|
|
|
// Display items
|
|
this.displayShopItems(shop.inventory);
|
|
|
|
// Show container
|
|
this.shopContainer.setVisible(true);
|
|
|
|
console.log(`🛒 Opened ${shop.name}`);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Close shop
|
|
*/
|
|
closeShop() {
|
|
this.isShopOpen = false;
|
|
this.currentShop = null;
|
|
this.shopContainer.setVisible(false);
|
|
|
|
console.log('🛒 Shop closed');
|
|
}
|
|
|
|
/**
|
|
* Display shop items
|
|
*/
|
|
displayShopItems(items, filter = 'all') {
|
|
// Clear previous items
|
|
this.itemListContainer.removeAll(true);
|
|
|
|
// Filter items
|
|
let filteredItems = items;
|
|
if (filter !== 'all') {
|
|
filteredItems = items.filter(item => item.category === filter);
|
|
}
|
|
|
|
// Display items (max 10 visible, scrollable)
|
|
const itemHeight = 50;
|
|
const startY = -150;
|
|
|
|
filteredItems.slice(0, 10).forEach((item, index) => {
|
|
const y = startY + (index * itemHeight);
|
|
|
|
// Item background
|
|
const itemBg = this.scene.add.rectangle(-400, y, 850, 45, 0x2d2d2d, 0.8);
|
|
itemBg.setStrokeStyle(1, 0x444444);
|
|
this.itemListContainer.add(itemBg);
|
|
|
|
// Item name
|
|
const nameText = this.scene.add.text(-380, y - 10, item.name, {
|
|
fontSize: '16px',
|
|
fontFamily: 'Arial',
|
|
color: '#ffffff'
|
|
});
|
|
this.itemListContainer.add(nameText);
|
|
|
|
// Stock
|
|
const stockText = this.scene.add.text(-380, y + 10, `Stock: ${item.stock}`, {
|
|
fontSize: '12px',
|
|
fontFamily: 'Arial',
|
|
color: '#888888'
|
|
});
|
|
this.itemListContainer.add(stockText);
|
|
|
|
// Price
|
|
const priceText = this.scene.add.text(200, y, `${item.price} Ž`, {
|
|
fontSize: '18px',
|
|
fontFamily: 'Arial',
|
|
color: '#FFD700',
|
|
fontStyle: 'bold'
|
|
});
|
|
priceText.setOrigin(0.5);
|
|
this.itemListContainer.add(priceText);
|
|
|
|
// Buy button
|
|
const isLocked = item.locked && !this.isShopRestored(this.currentShop.id);
|
|
const buyColor = isLocked ? 0x666666 : 0x228B22;
|
|
const buyBtn = this.scene.add.rectangle(350, y, 100, 35, buyColor);
|
|
buyBtn.setStrokeStyle(2, isLocked ? 0x999999 : 0x32CD32);
|
|
buyBtn.setInteractive();
|
|
buyBtn.on('pointerdown', () => {
|
|
if (isLocked) {
|
|
this.showNotification({
|
|
title: 'Shop Ruined',
|
|
text: `Restore the shop to unlock this item!`,
|
|
icon: '🏚️'
|
|
});
|
|
} else {
|
|
this.buyItem(item);
|
|
}
|
|
});
|
|
this.itemListContainer.add(buyBtn);
|
|
|
|
const buyText = this.scene.add.text(350, y, isLocked ? 'LOCKED' : 'BUY', {
|
|
fontSize: '14px',
|
|
fontFamily: 'Arial',
|
|
color: '#ffffff',
|
|
fontStyle: 'bold'
|
|
});
|
|
buyText.setOrigin(0.5);
|
|
this.itemListContainer.add(buyText);
|
|
});
|
|
}
|
|
|
|
isShopRestored(shopId) {
|
|
if (!this.scene.townRestorationSystem) return true; // Fallback if system missing
|
|
|
|
// Map shopId to buildingId
|
|
const mapping = {
|
|
'blacksmith': 'jakob_shop', // or whichever ID correctly matches TownRestorationSystem
|
|
'baker': 'lena_bakery',
|
|
'healer': 'dr_chen_clinic'
|
|
};
|
|
|
|
const buildingId = mapping[shopId];
|
|
if (!buildingId) return true; // General trader is always open
|
|
|
|
const building = this.scene.townRestorationSystem.buildings.get(buildingId);
|
|
return building ? building.isRestored : false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Filter by category
|
|
*/
|
|
filterByCategory(category) {
|
|
if (!this.currentShop) return;
|
|
this.displayShopItems(this.currentShop.inventory, category);
|
|
}
|
|
|
|
/**
|
|
* Buy item
|
|
*/
|
|
buyItem(item) {
|
|
// Check stock
|
|
if (item.stock <= 0) {
|
|
this.showNotification({
|
|
title: 'Out of Stock',
|
|
text: `${item.name} is out of stock!`,
|
|
icon: '📦'
|
|
});
|
|
return false;
|
|
}
|
|
|
|
// Calculate price with relationship discount
|
|
const finalPrice = this.calculatePrice(item.price);
|
|
|
|
// Check if player can afford
|
|
if (this.playerZlatniki < finalPrice) {
|
|
this.showNotification({
|
|
title: 'Not Enough Money',
|
|
text: `Need ${finalPrice}Ž to buy ${item.name}!`,
|
|
icon: '💰'
|
|
});
|
|
return false;
|
|
}
|
|
|
|
// Purchase!
|
|
this.playerZlatniki -= finalPrice;
|
|
item.stock--;
|
|
|
|
// TODO: Add item to player inventory
|
|
console.log(`✅ Purchased: ${item.name} for ${finalPrice}Ž`);
|
|
|
|
// Update UI
|
|
this.updateMoneyDisplay();
|
|
this.displayShopItems(this.currentShop.inventory);
|
|
|
|
this.showNotification({
|
|
title: 'Purchase Complete!',
|
|
text: `Bought ${item.name} for ${finalPrice}Ž!`,
|
|
icon: '✅'
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Calculate price with discounts
|
|
*/
|
|
calculatePrice(basePrice) {
|
|
// TODO: Apply relationship discounts
|
|
// For now, return base price
|
|
return basePrice;
|
|
}
|
|
|
|
/**
|
|
* Update money display
|
|
*/
|
|
updateMoneyDisplay() {
|
|
this.moneyText.setText(`💰 ${this.playerZlatniki} Zlatniki`);
|
|
}
|
|
|
|
/**
|
|
* Set player money
|
|
*/
|
|
setPlayerMoney(amount) {
|
|
this.playerZlatniki = amount;
|
|
this.updateMoneyDisplay();
|
|
}
|
|
|
|
/**
|
|
* Get shop info
|
|
*/
|
|
getShopInfo(shopId) {
|
|
return this.shops.get(shopId);
|
|
}
|
|
|
|
/**
|
|
* Get all shops
|
|
*/
|
|
getAllShops() {
|
|
return Array.from(this.shops.values());
|
|
}
|
|
|
|
/**
|
|
* Restock shop
|
|
*/
|
|
restockShop(shopId) {
|
|
const shop = this.shops.get(shopId);
|
|
if (!shop) return false;
|
|
|
|
shop.inventory.forEach(item => {
|
|
item.stock = Math.min(item.stock + 5, 100); // Restock +5, max 100
|
|
});
|
|
|
|
console.log(`📦 ${shop.name} restocked!`);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Helper: Show notification
|
|
*/
|
|
showNotification(notification) {
|
|
console.log(`📢 ${notification.icon} ${notification.title}: ${notification.text}`);
|
|
|
|
const ui = this.scene.scene.get('UIScene');
|
|
if (ui && ui.showNotification) {
|
|
ui.showNotification(notification);
|
|
}
|
|
}
|
|
}
|