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:
2025-09-23 20:13:01 +01:00
parent 3aaf0d0bb4
commit 0c8b4f9543
8 changed files with 892 additions and 339 deletions

View File

@@ -1,6 +1,6 @@
"""
Simplified Game mechanics for DuckHunt Bot
Basic duck spawning and timeout only
Game mechanics for DuckHunt Bot
Handles duck spawning, shooting, befriending, and other game actions
"""
import asyncio
@@ -10,7 +10,7 @@ import logging
class DuckGame:
"""Simplified game mechanics - just duck spawning"""
"""Game mechanics for DuckHunt - shooting, befriending, reloading"""
def __init__(self, bot, db):
self.bot = bot
@@ -31,15 +31,17 @@ class DuckGame:
self.logger.info("Game loops cancelled")
async def duck_spawn_loop(self):
"""Simple duck spawning loop"""
"""Duck spawning loop with responsive shutdown"""
try:
while True:
# Wait random time between spawns
# Wait random time between spawns, but in small chunks for responsiveness
min_wait = self.bot.get_config('duck_spawn_min', 300) # 5 minutes
max_wait = self.bot.get_config('duck_spawn_max', 900) # 15 minutes
wait_time = random.randint(min_wait, max_wait)
await asyncio.sleep(wait_time)
# Sleep in 1-second intervals to allow for quick cancellation
for _ in range(wait_time):
await asyncio.sleep(1)
# Spawn duck in random channel
channels = list(self.bot.channels_joined)
@@ -51,10 +53,11 @@ class DuckGame:
self.logger.info("Duck spawning loop cancelled")
async def duck_timeout_loop(self):
"""Simple duck timeout loop"""
"""Duck timeout loop with responsive shutdown"""
try:
while True:
await asyncio.sleep(10) # Check every 10 seconds
# Check every 2 seconds instead of 10 for more responsiveness
await asyncio.sleep(2)
current_time = time.time()
channels_to_clear = []
@@ -102,4 +105,184 @@ class DuckGame:
message = self.bot.messages.get('duck_spawn')
self.bot.send_message(channel, message)
self.logger.info(f"Duck spawned in {channel}")
self.logger.info(f"Duck spawned in {channel}")
def shoot_duck(self, nick, channel, player):
"""Handle shooting at a duck"""
# Check if gun is confiscated
if player.get('gun_confiscated', False):
return {
'success': False,
'message_key': 'bang_not_armed',
'message_args': {'nick': nick}
}
# Check ammo
if player.get('current_ammo', 0) <= 0:
return {
'success': False,
'message_key': 'bang_no_ammo',
'message_args': {'nick': nick}
}
# Check for duck
if channel not in self.ducks or not self.ducks[channel]:
# Wild shot - gun confiscated
player['current_ammo'] = player.get('current_ammo', 1) - 1
player['gun_confiscated'] = True
self.db.save_database()
return {
'success': False,
'message_key': 'bang_no_duck',
'message_args': {'nick': nick}
}
# Shoot at duck
player['current_ammo'] = player.get('current_ammo', 1) - 1
# Calculate hit chance using level-modified accuracy
modified_accuracy = self.bot.levels.get_modified_accuracy(player)
hit_chance = modified_accuracy / 100.0
if random.random() < hit_chance:
# Hit! Remove the duck
duck = self.ducks[channel].pop(0)
xp_gained = 10
old_level = self.bot.levels.calculate_player_level(player)
player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_shot'] = player.get('ducks_shot', 0) + 1
player['accuracy'] = min(player.get('accuracy', 65) + 1, 100)
# Check if player leveled up and update magazines if needed
new_level = self.bot.levels.calculate_player_level(player)
if new_level != old_level:
self.bot.levels.update_player_magazines(player)
self.db.save_database()
return {
'success': True,
'hit': True,
'message_key': 'bang_hit',
'message_args': {
'nick': nick,
'xp_gained': xp_gained,
'ducks_shot': player['ducks_shot']
}
}
else:
# Miss! Duck stays in the channel
player['accuracy'] = max(player.get('accuracy', 65) - 2, 10)
self.db.save_database()
return {
'success': True,
'hit': False,
'message_key': 'bang_miss',
'message_args': {'nick': nick}
}
def befriend_duck(self, nick, channel, player):
"""Handle befriending a duck"""
# Check for duck
if channel not in self.ducks or not self.ducks[channel]:
return {
'success': False,
'message_key': 'bef_no_duck',
'message_args': {'nick': nick}
}
# Check befriend success rate from config and level modifiers
base_rate = self.bot.get_config('befriend_success_rate', 75)
try:
if base_rate is not None:
base_rate = float(base_rate)
else:
base_rate = 75.0
except (ValueError, TypeError):
base_rate = 75.0
# Apply level-based modification to befriend rate
level_modified_rate = self.bot.levels.get_modified_befriend_rate(player, base_rate)
success_rate = level_modified_rate / 100.0
if random.random() < success_rate:
# Success - befriend the duck
duck = self.ducks[channel].pop(0)
# Lower XP gain than shooting (5 instead of 10)
xp_gained = 5
old_level = self.bot.levels.calculate_player_level(player)
player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_befriended'] = player.get('ducks_befriended', 0) + 1
# Check if player leveled up and update magazines if needed
new_level = self.bot.levels.calculate_player_level(player)
if new_level != old_level:
self.bot.levels.update_player_magazines(player)
self.db.save_database()
return {
'success': True,
'befriended': True,
'message_key': 'bef_success',
'message_args': {
'nick': nick,
'xp_gained': xp_gained,
'ducks_befriended': player['ducks_befriended']
}
}
else:
# Failure - duck flies away, remove from channel
duck = self.ducks[channel].pop(0)
self.db.save_database()
return {
'success': True,
'befriended': False,
'message_key': 'bef_failed',
'message_args': {'nick': nick}
}
def reload_gun(self, nick, channel, player):
"""Handle reloading a gun (switching to a new magazine)"""
if player.get('gun_confiscated', False):
return {
'success': False,
'message_key': 'reload_not_armed',
'message_args': {'nick': nick}
}
current_ammo = player.get('current_ammo', 0)
bullets_per_mag = player.get('bullets_per_magazine', 6)
# Check if current magazine is already full
if current_ammo >= bullets_per_mag:
return {
'success': False,
'message_key': 'reload_already_loaded',
'message_args': {'nick': nick}
}
# Check if they have spare magazines
total_magazines = player.get('magazines', 1)
if total_magazines <= 1: # Only the current magazine
return {
'success': False,
'message_key': 'reload_no_chargers',
'message_args': {'nick': nick}
}
# Reload: discard current magazine and load a new full one
player['current_ammo'] = bullets_per_mag
player['magazines'] = total_magazines - 1
self.db.save_database()
return {
'success': True,
'message_key': 'reload_success',
'message_args': {
'nick': nick,
'ammo': player['current_ammo'],
'max_ammo': bullets_per_mag,
'chargers': player['magazines'] - 1 # Spare magazines (excluding current)
}
}