Implement magazine system and inventory management
- Add level-based magazine system (3 mags at L1, 2 at L3-5, 1 at L6-8) - Replace ammo/chargers with current_ammo/magazines/bullets_per_magazine - Add inventory system for storing and using shop items - Add Magazine item to shop (15 XP, adds 1 magazine) - Auto-migrate existing players from old ammo system - Auto-update magazines when players level up - Fix method name bugs (get_player_level -> calculate_player_level)
This commit is contained in:
323
src/shop.py
323
src/shop.py
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
Shop system for DuckHunt Bot
|
||||
Handles loading items, purchasing, and item effects
|
||||
Handles loading items, purchasing, and item effects including player-vs-player actions
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
@@ -62,15 +63,33 @@ class ShopManager:
|
||||
return False
|
||||
return player_xp >= item['price']
|
||||
|
||||
def purchase_item(self, player: Dict[str, Any], item_id: int) -> Dict[str, Any]:
|
||||
def purchase_item(self, player: Dict[str, Any], item_id: int, target_player: Optional[Dict[str, Any]] = None, store_in_inventory: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Purchase an item and apply its effects to the player
|
||||
Purchase an item and either store in inventory or apply immediately
|
||||
Returns a result dictionary with success status and details
|
||||
"""
|
||||
item = self.get_item(item_id)
|
||||
if not item:
|
||||
return {"success": False, "error": "invalid_id", "message": "Invalid item ID"}
|
||||
|
||||
# If storing in inventory and item requires a target, that's invalid
|
||||
if store_in_inventory and item.get('target_required', False):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "invalid_storage",
|
||||
"message": f"{item['name']} cannot be stored - it targets other players",
|
||||
"item_name": item['name']
|
||||
}
|
||||
|
||||
# Check if item requires a target (only when not storing)
|
||||
if not store_in_inventory and item.get('target_required', False) and not target_player:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "target_required",
|
||||
"message": f"{item['name']} requires a target player",
|
||||
"item_name": item['name']
|
||||
}
|
||||
|
||||
player_xp = player.get('xp', 0)
|
||||
if player_xp < item['price']:
|
||||
return {
|
||||
@@ -85,16 +104,47 @@ class ShopManager:
|
||||
# Deduct XP
|
||||
player['xp'] = player_xp - item['price']
|
||||
|
||||
# Apply item effect
|
||||
effect_result = self._apply_item_effect(player, item)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"item_name": item['name'],
|
||||
"price": item['price'],
|
||||
"remaining_xp": player['xp'],
|
||||
"effect": effect_result
|
||||
}
|
||||
if store_in_inventory:
|
||||
# Add to inventory
|
||||
inventory = player.get('inventory', {})
|
||||
item_id_str = str(item_id)
|
||||
current_count = inventory.get(item_id_str, 0)
|
||||
inventory[item_id_str] = current_count + 1
|
||||
player['inventory'] = inventory
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"item_name": item['name'],
|
||||
"price": item['price'],
|
||||
"remaining_xp": player['xp'],
|
||||
"stored_in_inventory": True,
|
||||
"inventory_count": inventory[item_id_str]
|
||||
}
|
||||
else:
|
||||
# Apply effect immediately
|
||||
if item.get('target_required', False) and target_player:
|
||||
effect_result = self._apply_item_effect(target_player, item)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"item_name": item['name'],
|
||||
"price": item['price'],
|
||||
"remaining_xp": player['xp'],
|
||||
"effect": effect_result,
|
||||
"target_affected": True
|
||||
}
|
||||
else:
|
||||
# Apply effect to purchaser
|
||||
effect_result = self._apply_item_effect(player, item)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"item_name": item['name'],
|
||||
"price": item['price'],
|
||||
"remaining_xp": player['xp'],
|
||||
"effect": effect_result,
|
||||
"target_affected": False
|
||||
}
|
||||
|
||||
def _apply_item_effect(self, player: Dict[str, Any], item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Apply the effect of an item to a player"""
|
||||
@@ -102,16 +152,28 @@ class ShopManager:
|
||||
amount = item.get('amount', 0)
|
||||
|
||||
if item_type == 'ammo':
|
||||
# Add ammo up to max capacity
|
||||
current_ammo = player.get('ammo', 0)
|
||||
max_ammo = player.get('max_ammo', 6)
|
||||
new_ammo = min(current_ammo + amount, max_ammo)
|
||||
player['ammo'] = new_ammo
|
||||
# Add bullets to current magazine
|
||||
current_ammo = player.get('current_ammo', 0)
|
||||
bullets_per_mag = player.get('bullets_per_magazine', 6)
|
||||
new_ammo = min(current_ammo + amount, bullets_per_mag)
|
||||
added_bullets = new_ammo - current_ammo
|
||||
player['current_ammo'] = new_ammo
|
||||
return {
|
||||
"type": "ammo",
|
||||
"added": new_ammo - current_ammo,
|
||||
"added": added_bullets,
|
||||
"new_total": new_ammo,
|
||||
"max": max_ammo
|
||||
"max": bullets_per_mag
|
||||
}
|
||||
|
||||
elif item_type == 'magazine':
|
||||
# Add magazines to player's inventory
|
||||
current_magazines = player.get('magazines', 1)
|
||||
new_magazines = current_magazines + amount
|
||||
player['magazines'] = new_magazines
|
||||
return {
|
||||
"type": "magazine",
|
||||
"added": amount,
|
||||
"new_total": new_magazines
|
||||
}
|
||||
|
||||
elif item_type == 'accuracy':
|
||||
@@ -136,14 +198,233 @@ class ShopManager:
|
||||
"new_total": new_luck
|
||||
}
|
||||
|
||||
elif item_type == 'jam_resistance':
|
||||
# Reduce gun jamming chance (lower is better)
|
||||
current_jam = player.get('jam_chance', 5) # Default 5% jam chance
|
||||
new_jam = max(current_jam - amount, 0) # Can't go below 0%
|
||||
player['jam_chance'] = new_jam
|
||||
return {
|
||||
"type": "jam_resistance",
|
||||
"reduced": current_jam - new_jam,
|
||||
"new_total": new_jam
|
||||
}
|
||||
|
||||
elif item_type == 'max_ammo':
|
||||
# Increase maximum ammo capacity
|
||||
current_max = player.get('max_ammo', 6)
|
||||
new_max = current_max + amount
|
||||
player['max_ammo'] = new_max
|
||||
return {
|
||||
"type": "max_ammo",
|
||||
"added": amount,
|
||||
"new_total": new_max
|
||||
}
|
||||
|
||||
elif item_type == 'chargers':
|
||||
# Add reload chargers
|
||||
current_chargers = player.get('chargers', 2)
|
||||
new_chargers = current_chargers + amount
|
||||
player['chargers'] = new_chargers
|
||||
return {
|
||||
"type": "chargers",
|
||||
"added": amount,
|
||||
"new_total": new_chargers
|
||||
}
|
||||
|
||||
elif item_type == 'duck_attraction':
|
||||
# Increase chance of ducks appearing when this player is online
|
||||
current_attraction = player.get('duck_attraction', 0)
|
||||
new_attraction = current_attraction + amount
|
||||
player['duck_attraction'] = new_attraction
|
||||
return {
|
||||
"type": "duck_attraction",
|
||||
"added": amount,
|
||||
"new_total": new_attraction
|
||||
}
|
||||
|
||||
elif item_type == 'critical_hit':
|
||||
# Chance for critical hits (double XP)
|
||||
current_crit = player.get('critical_chance', 0)
|
||||
new_crit = min(current_crit + amount, 25) # Max 25% crit chance
|
||||
player['critical_chance'] = new_crit
|
||||
return {
|
||||
"type": "critical_hit",
|
||||
"added": new_crit - current_crit,
|
||||
"new_total": new_crit
|
||||
}
|
||||
|
||||
elif item_type == 'sabotage_jam':
|
||||
# Increase target's gun jamming chance temporarily
|
||||
current_jam = player.get('jam_chance', 5)
|
||||
new_jam = min(current_jam + amount, 50) # Max 50% jam chance
|
||||
player['jam_chance'] = new_jam
|
||||
|
||||
# Add temporary effect tracking
|
||||
if 'temporary_effects' not in player:
|
||||
player['temporary_effects'] = []
|
||||
|
||||
effect = {
|
||||
'type': 'jam_increase',
|
||||
'amount': amount,
|
||||
'expires_at': time.time() + item.get('duration', 5) * 60 # duration in minutes
|
||||
}
|
||||
player['temporary_effects'].append(effect)
|
||||
|
||||
return {
|
||||
"type": "sabotage_jam",
|
||||
"added": new_jam - current_jam,
|
||||
"new_total": new_jam,
|
||||
"duration": item.get('duration', 5)
|
||||
}
|
||||
|
||||
elif item_type == 'sabotage_accuracy':
|
||||
# Reduce target's accuracy temporarily
|
||||
current_acc = player.get('accuracy', 65)
|
||||
new_acc = max(current_acc + amount, 10) # Min 10% accuracy (amount is negative)
|
||||
player['accuracy'] = new_acc
|
||||
|
||||
# Add temporary effect tracking
|
||||
if 'temporary_effects' not in player:
|
||||
player['temporary_effects'] = []
|
||||
|
||||
effect = {
|
||||
'type': 'accuracy_reduction',
|
||||
'amount': amount,
|
||||
'expires_at': time.time() + item.get('duration', 3) * 60
|
||||
}
|
||||
player['temporary_effects'].append(effect)
|
||||
|
||||
return {
|
||||
"type": "sabotage_accuracy",
|
||||
"reduced": current_acc - new_acc,
|
||||
"new_total": new_acc,
|
||||
"duration": item.get('duration', 3)
|
||||
}
|
||||
|
||||
elif item_type == 'steal_ammo':
|
||||
# Steal ammo from target player
|
||||
current_ammo = player.get('ammo', 0)
|
||||
stolen = min(amount, current_ammo)
|
||||
player['ammo'] = max(current_ammo - stolen, 0)
|
||||
|
||||
return {
|
||||
"type": "steal_ammo",
|
||||
"stolen": stolen,
|
||||
"remaining": player['ammo']
|
||||
}
|
||||
|
||||
else:
|
||||
self.logger.warning(f"Unknown item type: {item_type}")
|
||||
return {"type": "unknown", "message": f"Unknown effect type: {item_type}"}
|
||||
|
||||
def use_inventory_item(self, player: Dict[str, Any], item_id: int, target_player: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Use an item from player's inventory
|
||||
Returns a result dictionary with success status and details
|
||||
"""
|
||||
item = self.get_item(item_id)
|
||||
if not item:
|
||||
return {"success": False, "error": "invalid_id", "message": "Invalid item ID"}
|
||||
|
||||
inventory = player.get('inventory', {})
|
||||
item_id_str = str(item_id)
|
||||
|
||||
if item_id_str not in inventory or inventory[item_id_str] <= 0:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "not_in_inventory",
|
||||
"message": f"You don't have any {item['name']} in your inventory",
|
||||
"item_name": item['name']
|
||||
}
|
||||
|
||||
# Check if item requires a target
|
||||
if item.get('target_required', False) and not target_player:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "target_required",
|
||||
"message": f"{item['name']} requires a target player",
|
||||
"item_name": item['name']
|
||||
}
|
||||
|
||||
# Remove item from inventory
|
||||
inventory[item_id_str] -= 1
|
||||
if inventory[item_id_str] <= 0:
|
||||
del inventory[item_id_str]
|
||||
player['inventory'] = inventory
|
||||
|
||||
# Apply effect
|
||||
if item.get('target_required', False) and target_player:
|
||||
effect_result = self._apply_item_effect(target_player, item)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"item_name": item['name'],
|
||||
"effect": effect_result,
|
||||
"target_affected": True,
|
||||
"remaining_in_inventory": inventory.get(item_id_str, 0)
|
||||
}
|
||||
else:
|
||||
# Apply effect to user
|
||||
effect_result = self._apply_item_effect(player, item)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"item_name": item['name'],
|
||||
"effect": effect_result,
|
||||
"target_affected": False,
|
||||
"remaining_in_inventory": inventory.get(item_id_str, 0)
|
||||
}
|
||||
|
||||
def get_inventory_display(self, player: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Get formatted inventory display for a player
|
||||
Returns dict with inventory info
|
||||
"""
|
||||
inventory = player.get('inventory', {})
|
||||
if not inventory:
|
||||
return {
|
||||
"empty": True,
|
||||
"message": "Your inventory is empty"
|
||||
}
|
||||
|
||||
items = []
|
||||
for item_id_str, quantity in inventory.items():
|
||||
item_id = int(item_id_str)
|
||||
item = self.get_item(item_id)
|
||||
if item:
|
||||
items.append({
|
||||
"id": item_id,
|
||||
"name": item['name'],
|
||||
"quantity": quantity,
|
||||
"description": item.get('description', 'No description')
|
||||
})
|
||||
|
||||
return {
|
||||
"empty": False,
|
||||
"items": items,
|
||||
"total_items": len(items)
|
||||
}
|
||||
|
||||
def reload_items(self) -> int:
|
||||
"""Reload items from file and return count"""
|
||||
old_count = len(self.items)
|
||||
self.load_items()
|
||||
new_count = len(self.items)
|
||||
self.logger.info(f"Shop reloaded: {old_count} -> {new_count} items")
|
||||
return new_count
|
||||
return new_count
|
||||
|
||||
def get_shop_display(self, player, message_manager):
|
||||
"""Get formatted shop display"""
|
||||
items = []
|
||||
for item_id, item in self.get_items().items():
|
||||
item_text = message_manager.get('shop_item_format',
|
||||
id=item_id,
|
||||
name=item['name'],
|
||||
price=item['price'])
|
||||
items.append(item_text)
|
||||
|
||||
shop_text = message_manager.get('shop_display',
|
||||
items=" | ".join(items),
|
||||
xp=player.get('xp', 0))
|
||||
|
||||
return shop_text
|
||||
Reference in New Issue
Block a user