feat: Add shop system, befriend command, and level system

- Added configurable shop system with shop.json
- Created ShopManager class for modular shop handling
- Implemented level system with levels.json for difficulty scaling
- Added multiple duck spawn messages with random selection
- Enhanced message system with color placeholders
- Added ducks_befriended tracking separate from ducks_shot
- Updated help system and admin commands
- All systems tested and working correctly
This commit is contained in:
2025-09-23 18:05:28 +01:00
parent de64756b6d
commit 3aaf0d0bb4
15 changed files with 733 additions and 60 deletions

View File

@@ -1,8 +1,3 @@
"""
Simplified DuckHunt IRC Bot - Core Features Only
Commands: !bang, !reload, !shop, !duckhelp, !rearm, !disarm, !ignore, !unignore, !ducklaunch
"""
import asyncio
import ssl
import json
@@ -19,6 +14,7 @@ from .utils import parse_irc_message, InputValidator, MessageManager
from .db import DuckDB
from .game import DuckGame
from .sasl import SASLHandler
from .shop import ShopManager
class DuckHuntBot:
@@ -41,12 +37,9 @@ class DuckHuntBot:
self.admins = [admin.lower() for admin in self.config.get('admins', ['colby'])]
# Simple shop items - hardcoded
self.shop_items = {
1: {"name": "Extra Shots", "price": 10, "description": "5 extra shots"},
2: {"name": "Accuracy Boost", "price": 20, "description": "+10% accuracy"},
3: {"name": "Lucky Charm", "price": 30, "description": "+5% duck spawn chance"}
}
# Initialize shop manager
shop_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shop.json')
self.shop = ShopManager(shop_file)
def get_config(self, path, default=None):
"""Get configuration value using dot notation"""
@@ -148,10 +141,12 @@ class DuckHuntBot:
if cmd == "bang":
await self.handle_bang(nick, channel, player)
elif cmd == "bef" or cmd == "befriend":
await self.handle_bef(nick, channel, player)
elif cmd == "reload":
await self.handle_reload(nick, channel, player)
elif cmd == "shop":
await self.handle_shop(nick, channel, player)
await self.handle_shop(nick, channel, player, args)
elif cmd == "duckhelp":
await self.handle_duckhelp(nick, channel, player)
elif cmd == "rearm" and self.is_admin(user):
@@ -164,6 +159,8 @@ class DuckHuntBot:
await self.handle_unignore(nick, channel, args)
elif cmd == "ducklaunch" and self.is_admin(user):
await self.handle_ducklaunch(nick, channel, args)
elif cmd == "reloadshop" and self.is_admin(user):
await self.handle_reloadshop(nick, channel, args)
async def handle_bang(self, nick, channel, player):
"""Handle !bang command"""
@@ -215,6 +212,44 @@ class DuckHuntBot:
self.db.save_database()
async def handle_bef(self, nick, channel, player):
"""Handle !bef (befriend) command"""
# Check for duck
if channel not in self.game.ducks or not self.game.ducks[channel]:
message = self.messages.get('bef_no_duck', nick=nick)
self.send_message(channel, message)
return
# Check befriend success rate from config (default 75%)
success_rate_config = self.get_config('befriend_success_rate', 75)
try:
success_rate = float(success_rate_config) / 100.0
except (ValueError, TypeError):
success_rate = 0.75 # 75% default
if random.random() < success_rate:
# Success - befriend the duck
duck = self.game.ducks[channel].pop(0)
# Lower XP gain than shooting (5 instead of 10)
xp_gained = 5
player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_befriended'] = player.get('ducks_befriended', 0) + 1
message = self.messages.get('bef_success',
nick=nick,
xp_gained=xp_gained,
ducks_befriended=player['ducks_befriended'])
self.send_message(channel, message)
else:
# Failure - duck flies away, remove from channel
duck = self.game.ducks[channel].pop(0)
message = self.messages.get('bef_failed', nick=nick)
self.send_message(channel, message)
self.db.save_database()
async def handle_reload(self, nick, channel, player):
"""Handle !reload command"""
if player.get('gun_confiscated', False):
@@ -243,10 +278,22 @@ class DuckHuntBot:
self.send_message(channel, message)
self.db.save_database()
async def handle_shop(self, nick, channel, player):
async def handle_shop(self, nick, channel, player, args=None):
"""Handle !shop command"""
# Handle buying: !shop buy <item_id>
if args and len(args) >= 2 and args[0].lower() == "buy":
try:
item_id = int(args[1])
await self.handle_shop_buy(nick, channel, player, item_id)
return
except (ValueError, IndexError):
message = self.messages.get('shop_buy_usage', nick=nick)
self.send_message(channel, message)
return
# Display shop items
items = []
for item_id, item in self.shop_items.items():
for item_id, item in self.shop.get_items().items():
item_text = self.messages.get('shop_item_format',
id=item_id,
name=item['name'],
@@ -259,6 +306,36 @@ class DuckHuntBot:
self.send_message(channel, shop_text)
async def handle_shop_buy(self, nick, channel, player, item_id):
"""Handle buying an item from the shop"""
# Use ShopManager to handle the purchase
result = self.shop.purchase_item(player, item_id)
if not result["success"]:
# Handle different error types
if result["error"] == "invalid_id":
message = self.messages.get('shop_buy_invalid_id', nick=nick)
elif result["error"] == "insufficient_xp":
message = self.messages.get('shop_buy_insufficient_xp',
nick=nick,
item_name=result["item_name"],
price=result["price"],
current_xp=result["current_xp"])
else:
message = f"{nick} > Error: {result['message']}"
self.send_message(channel, message)
return
# Purchase successful
message = self.messages.get('shop_buy_success',
nick=nick,
item_name=result["item_name"],
price=result["price"],
remaining_xp=result["remaining_xp"])
self.send_message(channel, message)
self.db.save_database()
async def handle_duckhelp(self, nick, channel, player):
"""Handle !duckhelp command"""
help_lines = [
@@ -357,6 +434,15 @@ class DuckHuntBot:
self.send_message(channel, admin_message)
self.send_message(channel, duck_message)
async def handle_reloadshop(self, nick, channel, args):
"""Handle !reloadshop admin command"""
old_count = len(self.shop.get_items())
new_count = self.shop.reload_items()
message = f"[ADMIN] Shop reloaded by {nick} - {new_count} items loaded"
self.send_message(channel, message)
self.logger.info(f"Shop reloaded by admin {nick}: {old_count} -> {new_count} items")
async def message_loop(self):
"""Main message processing loop"""
@@ -397,20 +483,15 @@ class DuckHuntBot:
# Start game loops
game_task = asyncio.create_task(self.game.start_game_loops())
message_task = asyncio.create_task(self.message_loop())
shutdown_task = asyncio.create_task(self.shutdown_event.wait())
self.logger.info("🦆 Bot is now running! Press Ctrl+C to stop.")
# Wait for shutdown signal or task completion
done, pending = await asyncio.wait(
[game_task, message_task, shutdown_task],
[game_task, message_task],
return_when=asyncio.FIRST_COMPLETED
)
if shutdown_task in done:
self.logger.info("🛑 Shutdown signal received, cleaning up...")
await self._graceful_shutdown()
# Cancel remaining tasks
for task in pending:
if not task.done():