Add new duck types and items
This commit is contained in:
@@ -50,6 +50,56 @@
|
|||||||
"timeout": 20,
|
"timeout": 20,
|
||||||
"xp": 12,
|
"xp": 12,
|
||||||
"drop_chance": 0.25
|
"drop_chance": 0.25
|
||||||
|
},
|
||||||
|
"concrete": {
|
||||||
|
"chance": 0.08,
|
||||||
|
"hp": 3,
|
||||||
|
"xp": 3,
|
||||||
|
"timeout": 90,
|
||||||
|
"drop_chance": 0.15
|
||||||
|
},
|
||||||
|
"holy_grail": {
|
||||||
|
"chance": 0.03,
|
||||||
|
"hp": 8,
|
||||||
|
"xp": 10,
|
||||||
|
"timeout": 120,
|
||||||
|
"drop_chance": 0.35
|
||||||
|
},
|
||||||
|
"diamond": {
|
||||||
|
"chance": 0.01,
|
||||||
|
"hp": 10,
|
||||||
|
"xp": 15,
|
||||||
|
"timeout": 150,
|
||||||
|
"drop_chance": 0.5
|
||||||
|
},
|
||||||
|
"explosive": {
|
||||||
|
"chance": 0.02,
|
||||||
|
"hp": 1,
|
||||||
|
"xp": 20,
|
||||||
|
"timeout": 60,
|
||||||
|
"drop_chance": 0.25
|
||||||
|
},
|
||||||
|
"poisonous": {
|
||||||
|
"chance": 0.02,
|
||||||
|
"hp": 1,
|
||||||
|
"xp": 8,
|
||||||
|
"timeout": 60,
|
||||||
|
"drop_chance": 0.25
|
||||||
|
},
|
||||||
|
"radioactive": {
|
||||||
|
"chance": 0.005,
|
||||||
|
"hp": 1,
|
||||||
|
"xp": 15,
|
||||||
|
"timeout": 60,
|
||||||
|
"drop_chance": 0.35
|
||||||
|
},
|
||||||
|
"couple": {
|
||||||
|
"chance": 0.03,
|
||||||
|
"timeout": 60
|
||||||
|
},
|
||||||
|
"family": {
|
||||||
|
"chance": 0.015,
|
||||||
|
"timeout": 60
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"item_drops": {
|
"item_drops": {
|
||||||
|
|||||||
@@ -81,6 +81,22 @@
|
|||||||
"use_splash_water": "{nick} > *SPLASH* You soaked {target_nick} with water! They can't shoot for {duration} minutes.",
|
"use_splash_water": "{nick} > *SPLASH* You soaked {target_nick} with water! They can't shoot for {duration} minutes.",
|
||||||
"use_dry_clothes": "{nick} > You changed into dry clothes! Ready to hunt again.",
|
"use_dry_clothes": "{nick} > You changed into dry clothes! Ready to hunt again.",
|
||||||
"use_dry_clothes_not_needed": "{nick} > You weren't wet - no need for new clothes.",
|
"use_dry_clothes_not_needed": "{nick} > You weren't wet - no need for new clothes.",
|
||||||
|
"use_perfect_aim": "{nick} > You line up the perfect shot. {green}[Perfect aim for {duration_minutes} minutes]{reset}",
|
||||||
|
"use_duck_radar": "{nick} > Duck Radar online. I'll DM you when a duck spawns here. {green}[{duration_hours} hours]{reset}",
|
||||||
|
"use_summon_duck": "{nick} > You call out to the pond... a duck should show up in {channel}.",
|
||||||
|
"use_summon_duck_delayed": "{nick} > You set a decoy. A duck should show up in {channel} in about {delay_minutes} minutes.",
|
||||||
|
"radar_alert": "Duck Radar: A duck has spawned in {channel}!",
|
||||||
|
|
||||||
|
"bang_hit_concrete": "{nick} > {red}*BANG*{reset} You hit a CONCRETE DUCK! [{hp_remaining} HP remaining] {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
|
||||||
|
"bang_hit_concrete_killed": "{nick} > {red}*BANG*{reset} You shattered the CONCRETE DUCK! {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
|
||||||
|
"bang_hit_holy_grail": "{nick} > {red}*BANG*{reset} You hit the HOLY GRAIL DUCK! [{hp_remaining} HP remaining] {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
|
||||||
|
"bang_hit_holy_grail_killed": "{nick} > {red}*BANG*{reset} You claimed the HOLY GRAIL DUCK! {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
|
||||||
|
"bang_hit_diamond": "{nick} > {red}*BANG*{reset} You hit a DIAMOND DUCK! [{hp_remaining} HP remaining] {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
|
||||||
|
"bang_hit_diamond_killed": "{nick} > {red}*BANG*{reset} You bagged the DIAMOND DUCK! {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
|
||||||
|
"bang_hit_explosive": "{nick} > {red}*BANG*{reset} You shot an EXPLOSIVE DUCK! {red}[BOOM - eliminated for 2 hours]{reset} {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
|
||||||
|
"bef_poisoned": "{nick} > You befriended the duck... but it was poisonous! {red}[Poisoned for {duration_hours} hours]{reset}",
|
||||||
|
"player_eliminated": "{nick} > You're eliminated and can't hunt right now.",
|
||||||
|
"player_poisoned": "{nick} > You're poisoned and can't hunt right now.",
|
||||||
"gift_success_generic": "{nick} > Successfully gave {item_name} to {target_nick}!",
|
"gift_success_generic": "{nick} > Successfully gave {item_name} to {target_nick}!",
|
||||||
"gift_ammo": "{nick} > Gave {amount} bullet(s) to {target_nick}! What a generous hunter.",
|
"gift_ammo": "{nick} > Gave {amount} bullet(s) to {target_nick}! What a generous hunter.",
|
||||||
"gift_magazine": "{nick} > Gave 1 magazine to {target_nick}! Sharing the ammo love.",
|
"gift_magazine": "{nick} > Gave 1 magazine to {target_nick}! Sharing the ammo love.",
|
||||||
|
|||||||
50
shop.json
50
shop.json
@@ -61,6 +61,56 @@
|
|||||||
"price": 30,
|
"price": 30,
|
||||||
"description": "Change into dry clothes - allows shooting again after being soaked",
|
"description": "Change into dry clothes - allows shooting again after being soaked",
|
||||||
"type": "dry_clothes"
|
"type": "dry_clothes"
|
||||||
|
},
|
||||||
|
|
||||||
|
"10": {
|
||||||
|
"name": "Sniper Rifle",
|
||||||
|
"price": 200,
|
||||||
|
"description": "Perfect aim - your shots won't miss for 30 minutes",
|
||||||
|
"type": "perfect_aim",
|
||||||
|
"duration": 1800
|
||||||
|
},
|
||||||
|
"11": {
|
||||||
|
"name": "Sniper Scope",
|
||||||
|
"price": 350,
|
||||||
|
"description": "Perfect aim - your shots won't miss for 60 minutes",
|
||||||
|
"type": "perfect_aim",
|
||||||
|
"duration": 3600
|
||||||
|
},
|
||||||
|
"12": {
|
||||||
|
"name": "Duck Whistle",
|
||||||
|
"price": 120,
|
||||||
|
"description": "Instantly summons a duck (if none are present)",
|
||||||
|
"type": "summon_duck",
|
||||||
|
"delay": 0
|
||||||
|
},
|
||||||
|
"13": {
|
||||||
|
"name": "Duck Caller",
|
||||||
|
"price": 200,
|
||||||
|
"description": "Instantly summons a duck (if none are present)",
|
||||||
|
"type": "summon_duck",
|
||||||
|
"delay": 0
|
||||||
|
},
|
||||||
|
"14": {
|
||||||
|
"name": "Duck Horn",
|
||||||
|
"price": 300,
|
||||||
|
"description": "Instantly summons a duck (if none are present)",
|
||||||
|
"type": "summon_duck",
|
||||||
|
"delay": 0
|
||||||
|
},
|
||||||
|
"15": {
|
||||||
|
"name": "Duck Decoy",
|
||||||
|
"price": 80,
|
||||||
|
"description": "Summons a duck in 1 hour (if none are present)",
|
||||||
|
"type": "summon_duck",
|
||||||
|
"delay": 3600
|
||||||
|
},
|
||||||
|
"16": {
|
||||||
|
"name": "Duck Radar",
|
||||||
|
"price": 150,
|
||||||
|
"description": "DM alert when a duck spawns in this channel (lasts 6 hours)",
|
||||||
|
"type": "duck_radar",
|
||||||
|
"duration": 21600
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1304,6 +1304,42 @@ class DuckHuntBot:
|
|||||||
message = self.messages.get('use_dry_clothes', nick=nick)
|
message = self.messages.get('use_dry_clothes', nick=nick)
|
||||||
else:
|
else:
|
||||||
message = self.messages.get('use_dry_clothes_not_needed', nick=nick)
|
message = self.messages.get('use_dry_clothes_not_needed', nick=nick)
|
||||||
|
elif effect_type == 'perfect_aim':
|
||||||
|
duration_seconds = int(effect.get('duration', 1800))
|
||||||
|
minutes = max(1, duration_seconds // 60)
|
||||||
|
message = self.messages.get('use_perfect_aim', nick=nick, duration_minutes=minutes)
|
||||||
|
elif effect_type == 'duck_radar':
|
||||||
|
duration_seconds = int(effect.get('duration', 21600))
|
||||||
|
hours = max(1, duration_seconds // 3600)
|
||||||
|
message = self.messages.get('use_duck_radar', nick=nick, duration_hours=hours)
|
||||||
|
elif effect_type == 'summon_duck':
|
||||||
|
# Summoning needs a channel context. If used in PM, pick the first configured channel.
|
||||||
|
delay = int(effect.get('delay', 0))
|
||||||
|
target_channel = channel
|
||||||
|
if not isinstance(target_channel, str) or not target_channel.startswith('#'):
|
||||||
|
channels = self.get_config('connection.channels', []) or []
|
||||||
|
target_channel = channels[0] if channels else None
|
||||||
|
|
||||||
|
if not target_channel:
|
||||||
|
message = f"{nick} > I don't know which channel to summon a duck in. Use this in a channel."
|
||||||
|
else:
|
||||||
|
if delay <= 0:
|
||||||
|
await self.game.spawn_duck(target_channel)
|
||||||
|
message = self.messages.get('use_summon_duck', nick=nick, channel=target_channel)
|
||||||
|
else:
|
||||||
|
async def delayed_summon():
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
if target_channel in self.channels_joined:
|
||||||
|
await self.game.spawn_duck(target_channel)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
asyncio.create_task(delayed_summon())
|
||||||
|
minutes = max(1, delay // 60)
|
||||||
|
message = self.messages.get('use_summon_duck_delayed', nick=nick, channel=target_channel, delay_minutes=minutes)
|
||||||
elif result.get("target_affected"):
|
elif result.get("target_affected"):
|
||||||
# Check if it's a gift (beneficial effect to target)
|
# Check if it's a gift (beneficial effect to target)
|
||||||
if effect.get('is_gift', False):
|
if effect.get('is_gift', False):
|
||||||
|
|||||||
310
src/game.py
310
src/game.py
@@ -117,62 +117,138 @@ class DuckGame:
|
|||||||
if channel not in self.ducks:
|
if channel not in self.ducks:
|
||||||
self.ducks[channel] = []
|
self.ducks[channel] = []
|
||||||
|
|
||||||
# Don't spawn if there's already a duck
|
# Don't spawn if there are already ducks present
|
||||||
if self.ducks[channel]:
|
if self.ducks[channel]:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Determine duck type randomly
|
duck_type = self._choose_duck_type()
|
||||||
golden_chance = self.bot.get_config('golden_duck_chance', 0.15)
|
|
||||||
fast_chance = self.bot.get_config('fast_duck_chance', 0.25)
|
|
||||||
|
|
||||||
rand = random.random()
|
# Special spawns that create multiple normal ducks.
|
||||||
if rand < golden_chance:
|
if duck_type in ('couple', 'family'):
|
||||||
# Golden duck - high HP, high XP
|
count = 2 if duck_type == 'couple' else random.randint(3, 4)
|
||||||
min_hp = self.bot.get_config('golden_duck_min_hp', 3)
|
for _ in range(count):
|
||||||
max_hp = self.bot.get_config('golden_duck_max_hp', 5)
|
duck = self._create_duck(channel, 'normal')
|
||||||
|
self.ducks[channel].append(duck)
|
||||||
|
self.logger.info(f"{duck_type} spawned {count} ducks in {channel}")
|
||||||
|
else:
|
||||||
|
duck = self._create_duck(channel, duck_type)
|
||||||
|
self.ducks[channel].append(duck)
|
||||||
|
hp = duck.get('max_hp', 1)
|
||||||
|
if duck_type != 'normal':
|
||||||
|
self.logger.info(f"{duck_type} duck (hidden) spawned in {channel} with {hp} HP")
|
||||||
|
else:
|
||||||
|
self.logger.info(f"Normal duck spawned in {channel}")
|
||||||
|
|
||||||
|
# Notify players with Duck Radar
|
||||||
|
try:
|
||||||
|
for player_name, player_data in self.db.get_players_for_channel(channel).items():
|
||||||
|
if self._has_active_effect(player_data, 'duck_radar'):
|
||||||
|
self.bot.send_message(player_name, self.bot.messages.get('radar_alert', channel=channel))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# All duck types use the same spawn message - type is hidden!
|
||||||
|
message = self.bot.messages.get('duck_spawn')
|
||||||
|
self.bot.send_message(channel, message)
|
||||||
|
|
||||||
|
def _choose_duck_type(self):
|
||||||
|
"""Choose a duck type using duck_types.*.chance (with legacy fallbacks)."""
|
||||||
|
try:
|
||||||
|
duck_types = self.bot.get_config('duck_types', {}) or {}
|
||||||
|
if not isinstance(duck_types, dict):
|
||||||
|
duck_types = {}
|
||||||
|
|
||||||
|
weighted = []
|
||||||
|
total = 0.0
|
||||||
|
|
||||||
|
for dtype, cfg in duck_types.items():
|
||||||
|
if dtype == 'normal':
|
||||||
|
continue
|
||||||
|
chance = None
|
||||||
|
if isinstance(cfg, dict):
|
||||||
|
chance = cfg.get('chance')
|
||||||
|
|
||||||
|
# Legacy fallbacks
|
||||||
|
if chance is None and dtype == 'golden':
|
||||||
|
chance = self.bot.get_config('golden_duck_chance', None)
|
||||||
|
if chance is None and dtype == 'fast':
|
||||||
|
chance = self.bot.get_config('fast_duck_chance', None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
chance = float(chance)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
chance = 0.0
|
||||||
|
|
||||||
|
if chance > 0:
|
||||||
|
weighted.append((dtype, chance))
|
||||||
|
total += chance
|
||||||
|
|
||||||
|
if total <= 0:
|
||||||
|
return 'normal'
|
||||||
|
|
||||||
|
r = random.random()
|
||||||
|
if r >= min(1.0, total):
|
||||||
|
return 'normal'
|
||||||
|
|
||||||
|
pick = random.random() * total
|
||||||
|
cumulative = 0.0
|
||||||
|
for dtype, weight in weighted:
|
||||||
|
cumulative += weight
|
||||||
|
if pick <= cumulative:
|
||||||
|
return dtype
|
||||||
|
return weighted[-1][0]
|
||||||
|
except Exception:
|
||||||
|
return 'normal'
|
||||||
|
|
||||||
|
def _create_duck(self, channel, duck_type):
|
||||||
|
"""Create a duck dict for a given type."""
|
||||||
|
cfg = self.bot.get_config(f'duck_types.{duck_type}', {}) or {}
|
||||||
|
if not isinstance(cfg, dict):
|
||||||
|
cfg = {}
|
||||||
|
|
||||||
|
# Legacy golden HP keys
|
||||||
|
if duck_type == 'golden' and ('min_hp' not in cfg and 'max_hp' not in cfg):
|
||||||
|
cfg = dict(cfg)
|
||||||
|
cfg['min_hp'] = self.bot.get_config('golden_duck_min_hp', 3)
|
||||||
|
cfg['max_hp'] = self.bot.get_config('golden_duck_max_hp', 5)
|
||||||
|
|
||||||
|
min_hp = cfg.get('min_hp', cfg.get('hp', 1))
|
||||||
|
max_hp = cfg.get('max_hp', cfg.get('hp', 1))
|
||||||
|
try:
|
||||||
|
min_hp = int(min_hp)
|
||||||
|
max_hp = int(max_hp)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
min_hp = 1
|
||||||
|
max_hp = 1
|
||||||
|
min_hp = max(1, min_hp)
|
||||||
|
max_hp = max(min_hp, max_hp)
|
||||||
hp = random.randint(min_hp, max_hp)
|
hp = random.randint(min_hp, max_hp)
|
||||||
duck_type = 'golden'
|
|
||||||
duck = {
|
return {
|
||||||
'id': f"golden_duck_{int(time.time())}_{random.randint(1000, 9999)}",
|
'id': f"{duck_type}_duck_{int(time.time())}_{random.randint(1000, 9999)}",
|
||||||
'spawn_time': time.time(),
|
'spawn_time': time.time(),
|
||||||
'channel': channel,
|
'channel': channel,
|
||||||
'duck_type': duck_type,
|
'duck_type': duck_type,
|
||||||
'max_hp': hp,
|
'max_hp': hp,
|
||||||
'current_hp': hp
|
'current_hp': hp
|
||||||
}
|
}
|
||||||
self.logger.info(f"Golden duck (hidden) spawned in {channel} with {hp} HP")
|
|
||||||
elif rand < golden_chance + fast_chance:
|
|
||||||
# Fast duck - normal HP, flies away faster
|
|
||||||
duck_type = 'fast'
|
|
||||||
duck = {
|
|
||||||
'id': f"fast_duck_{int(time.time())}_{random.randint(1000, 9999)}",
|
|
||||||
'spawn_time': time.time(),
|
|
||||||
'channel': channel,
|
|
||||||
'duck_type': duck_type,
|
|
||||||
'max_hp': 1,
|
|
||||||
'current_hp': 1
|
|
||||||
}
|
|
||||||
self.logger.info(f"Fast duck (hidden) spawned in {channel}")
|
|
||||||
else:
|
|
||||||
# Normal duck
|
|
||||||
duck_type = 'normal'
|
|
||||||
duck = {
|
|
||||||
'id': f"duck_{int(time.time())}_{random.randint(1000, 9999)}",
|
|
||||||
'spawn_time': time.time(),
|
|
||||||
'channel': channel,
|
|
||||||
'duck_type': duck_type,
|
|
||||||
'max_hp': 1,
|
|
||||||
'current_hp': 1
|
|
||||||
}
|
|
||||||
self.logger.info(f"Normal duck spawned in {channel}")
|
|
||||||
|
|
||||||
# All duck types use the same spawn message - type is hidden!
|
|
||||||
message = self.bot.messages.get('duck_spawn')
|
|
||||||
self.ducks[channel].append(duck)
|
|
||||||
self.bot.send_message(channel, message)
|
|
||||||
|
|
||||||
def shoot_duck(self, nick, channel, player):
|
def shoot_duck(self, nick, channel, player):
|
||||||
"""Handle shooting at a duck"""
|
"""Handle shooting at a duck"""
|
||||||
|
# Status effects
|
||||||
|
if self._has_active_effect(player, 'eliminated'):
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message_key': 'player_eliminated',
|
||||||
|
'message_args': {'nick': nick}
|
||||||
|
}
|
||||||
|
if self._has_active_effect(player, 'poisoned'):
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message_key': 'player_poisoned',
|
||||||
|
'message_args': {'nick': nick}
|
||||||
|
}
|
||||||
|
|
||||||
# Check if gun is confiscated
|
# Check if gun is confiscated
|
||||||
if player.get('gun_confiscated', False):
|
if player.get('gun_confiscated', False):
|
||||||
return {
|
return {
|
||||||
@@ -233,55 +309,72 @@ class DuckGame:
|
|||||||
# Calculate hit chance using level-modified accuracy
|
# Calculate hit chance using level-modified accuracy
|
||||||
modified_accuracy = self.bot.levels.get_modified_accuracy(player)
|
modified_accuracy = self.bot.levels.get_modified_accuracy(player)
|
||||||
hit_chance = modified_accuracy / 100.0
|
hit_chance = modified_accuracy / 100.0
|
||||||
|
if self._has_active_effect(player, 'perfect_aim'):
|
||||||
|
hit_chance = 1.0
|
||||||
if random.random() < hit_chance:
|
if random.random() < hit_chance:
|
||||||
# Hit! Get the duck and reveal its type
|
# Hit! Get the duck and reveal its type
|
||||||
duck = self.ducks[channel][0]
|
duck = self.ducks[channel][0]
|
||||||
duck_type = duck.get('duck_type', 'normal')
|
duck_type = duck.get('duck_type', 'normal')
|
||||||
|
|
||||||
if duck_type == 'golden':
|
# Multi-HP ducks: treat as "boss" style.
|
||||||
# Golden duck - multi-hit with high XP
|
if duck.get('max_hp', 1) > 1:
|
||||||
duck['current_hp'] -= 1
|
duck['current_hp'] -= 1
|
||||||
xp_gained = self.bot.get_config('golden_duck_xp', 15)
|
per_hit_xp = self._get_duck_xp_per_hit(duck_type)
|
||||||
|
|
||||||
if duck['current_hp'] > 0:
|
if duck['current_hp'] > 0:
|
||||||
# Still alive, reveal it's golden but don't remove
|
accuracy_gain = self.bot.get_config('gameplay.accuracy_gain_on_hit', self.bot.get_config('accuracy_gain_on_hit', 1))
|
||||||
accuracy_gain = self.bot.get_config('accuracy_gain_on_hit', 1)
|
max_accuracy = self.bot.get_config('gameplay.max_accuracy', self.bot.get_config('max_accuracy', 100))
|
||||||
max_accuracy = self.bot.get_config('max_accuracy', 100)
|
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('player_defaults.accuracy', 75)) + accuracy_gain, max_accuracy)
|
||||||
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) + accuracy_gain, max_accuracy)
|
|
||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
|
|
||||||
|
message_key = {
|
||||||
|
'golden': 'bang_hit_golden',
|
||||||
|
'concrete': 'bang_hit_concrete',
|
||||||
|
'holy_grail': 'bang_hit_holy_grail',
|
||||||
|
'diamond': 'bang_hit_diamond'
|
||||||
|
}.get(duck_type, 'bang_hit_golden')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'hit': True,
|
'hit': True,
|
||||||
'message_key': 'bang_hit_golden',
|
'message_key': message_key,
|
||||||
'message_args': {
|
'message_args': {
|
||||||
'nick': nick,
|
'nick': nick,
|
||||||
'hp_remaining': duck['current_hp'],
|
'hp_remaining': duck['current_hp'],
|
||||||
'xp_gained': xp_gained
|
'xp_gained': per_hit_xp,
|
||||||
|
'ducks_shot': player.get('ducks_shot', 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Killed!
|
||||||
|
self.ducks[channel].pop(0)
|
||||||
|
xp_gained = per_hit_xp * int(duck.get('max_hp', 1))
|
||||||
|
message_key = {
|
||||||
|
'golden': 'bang_hit_golden_killed',
|
||||||
|
'concrete': 'bang_hit_concrete_killed',
|
||||||
|
'holy_grail': 'bang_hit_holy_grail_killed',
|
||||||
|
'diamond': 'bang_hit_diamond_killed'
|
||||||
|
}.get(duck_type, 'bang_hit_golden_killed')
|
||||||
else:
|
else:
|
||||||
# Golden duck killed!
|
# Single-HP ducks
|
||||||
self.ducks[channel].pop(0)
|
self.ducks[channel].pop(0)
|
||||||
xp_gained = xp_gained * duck['max_hp'] # Bonus XP for killing
|
xp_gained = self._get_duck_xp_per_hit(duck_type)
|
||||||
message_key = 'bang_hit_golden_killed'
|
message_key = {
|
||||||
elif duck_type == 'fast':
|
'normal': 'bang_hit',
|
||||||
# Fast duck - normal HP but higher XP
|
'fast': 'bang_hit_fast',
|
||||||
self.ducks[channel].pop(0)
|
'explosive': 'bang_hit_explosive'
|
||||||
xp_gained = self.bot.get_config('fast_duck_xp', 12)
|
}.get(duck_type, 'bang_hit')
|
||||||
message_key = 'bang_hit_fast'
|
|
||||||
else:
|
if duck_type == 'explosive':
|
||||||
# Normal duck
|
self._add_temporary_effect(player, 'eliminated', 2 * 3600)
|
||||||
self.ducks[channel].pop(0)
|
|
||||||
xp_gained = self.bot.get_config('normal_duck_xp', 10)
|
|
||||||
message_key = 'bang_hit'
|
|
||||||
|
|
||||||
# Apply XP and level changes
|
# Apply XP and level changes
|
||||||
old_level = self.bot.levels.calculate_player_level(player)
|
old_level = self.bot.levels.calculate_player_level(player)
|
||||||
player['xp'] = player.get('xp', 0) + xp_gained
|
player['xp'] = player.get('xp', 0) + xp_gained
|
||||||
player['ducks_shot'] = player.get('ducks_shot', 0) + 1
|
player['ducks_shot'] = player.get('ducks_shot', 0) + 1
|
||||||
accuracy_gain = self.bot.get_config('accuracy_gain_on_hit', 1)
|
accuracy_gain = self.bot.get_config('gameplay.accuracy_gain_on_hit', self.bot.get_config('accuracy_gain_on_hit', 1))
|
||||||
max_accuracy = self.bot.get_config('max_accuracy', 100)
|
max_accuracy = self.bot.get_config('gameplay.max_accuracy', self.bot.get_config('max_accuracy', 100))
|
||||||
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) + accuracy_gain, max_accuracy)
|
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('player_defaults.accuracy', 75)) + accuracy_gain, max_accuracy)
|
||||||
|
|
||||||
# Check if player leveled up and update magazines if needed
|
# Check if player leveled up and update magazines if needed
|
||||||
new_level = self.bot.levels.calculate_player_level(player)
|
new_level = self.bot.levels.calculate_player_level(player)
|
||||||
@@ -391,6 +484,20 @@ class DuckGame:
|
|||||||
|
|
||||||
def befriend_duck(self, nick, channel, player):
|
def befriend_duck(self, nick, channel, player):
|
||||||
"""Handle befriending a duck"""
|
"""Handle befriending a duck"""
|
||||||
|
# Status effects
|
||||||
|
if self._has_active_effect(player, 'eliminated'):
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message_key': 'player_eliminated',
|
||||||
|
'message_args': {'nick': nick}
|
||||||
|
}
|
||||||
|
if self._has_active_effect(player, 'poisoned'):
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message_key': 'player_poisoned',
|
||||||
|
'message_args': {'nick': nick}
|
||||||
|
}
|
||||||
|
|
||||||
# Check for duck
|
# Check for duck
|
||||||
if channel not in self.ducks or not self.ducks[channel]:
|
if channel not in self.ducks or not self.ducks[channel]:
|
||||||
return {
|
return {
|
||||||
@@ -417,6 +524,34 @@ class DuckGame:
|
|||||||
# Success - befriend the duck
|
# Success - befriend the duck
|
||||||
duck = self.ducks[channel].pop(0)
|
duck = self.ducks[channel].pop(0)
|
||||||
|
|
||||||
|
duck_type = duck.get('duck_type', 'normal')
|
||||||
|
|
||||||
|
# Poison effects
|
||||||
|
if duck_type == 'poisonous':
|
||||||
|
self._add_temporary_effect(player, 'poisoned', 2 * 3600)
|
||||||
|
self.db.save_database()
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'befriended': True,
|
||||||
|
'message_key': 'bef_poisoned',
|
||||||
|
'message_args': {
|
||||||
|
'nick': nick,
|
||||||
|
'duration_hours': 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if duck_type == 'radioactive':
|
||||||
|
self._add_temporary_effect(player, 'poisoned', 8 * 3600)
|
||||||
|
self.db.save_database()
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'befriended': True,
|
||||||
|
'message_key': 'bef_poisoned',
|
||||||
|
'message_args': {
|
||||||
|
'nick': nick,
|
||||||
|
'duration_hours': 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Lower XP gain than shooting
|
# Lower XP gain than shooting
|
||||||
xp_gained = self.bot.get_config('gameplay.befriend_xp', 5)
|
xp_gained = self.bot.get_config('gameplay.befriend_xp', 5)
|
||||||
old_level = self.bot.levels.calculate_player_level(player)
|
old_level = self.bot.levels.calculate_player_level(player)
|
||||||
@@ -602,12 +737,13 @@ class DuckGame:
|
|||||||
if random.random() > drop_chance:
|
if random.random() > drop_chance:
|
||||||
return None # No drop
|
return None # No drop
|
||||||
|
|
||||||
# Get drop table for this duck type
|
# Get drop table for this duck type (fallback to normal)
|
||||||
drop_table_key = f'{duck_type}_duck_drops'
|
drop_table_key = f'{duck_type}_duck_drops'
|
||||||
drop_table = self.bot.get_config(f'item_drops.{drop_table_key}', [])
|
drop_table = self.bot.get_config(f'item_drops.{drop_table_key}', [])
|
||||||
|
if not drop_table:
|
||||||
|
drop_table = self.bot.get_config('item_drops.normal_duck_drops', [])
|
||||||
|
|
||||||
if not drop_table:
|
if not drop_table:
|
||||||
self.logger.warning(f"No drop table found for {duck_type} duck")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Weighted random selection
|
# Weighted random selection
|
||||||
@@ -647,3 +783,39 @@ class DuckGame:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error in _check_item_drop: {e}")
|
self.logger.error(f"Error in _check_item_drop: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _has_active_effect(self, player, effect_type):
|
||||||
|
import time
|
||||||
|
current_time = time.time()
|
||||||
|
effects = player.get('temporary_effects', [])
|
||||||
|
for effect in effects:
|
||||||
|
if effect.get('type') == effect_type and effect.get('expires_at', 0) > current_time:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _add_temporary_effect(self, player, effect_type, duration_seconds):
|
||||||
|
import time
|
||||||
|
if 'temporary_effects' not in player:
|
||||||
|
player['temporary_effects'] = []
|
||||||
|
duration_seconds = int(duration_seconds)
|
||||||
|
duration_seconds = max(1, min(duration_seconds, 7 * 24 * 3600)) # cap 7 days
|
||||||
|
player['temporary_effects'].append({
|
||||||
|
'type': effect_type,
|
||||||
|
'expires_at': time.time() + duration_seconds
|
||||||
|
})
|
||||||
|
|
||||||
|
def _get_duck_xp_per_hit(self, duck_type):
|
||||||
|
"""Get XP value for a duck type (supports duck_types.*.xp and legacy keys)."""
|
||||||
|
xp = self.bot.get_config(f'duck_types.{duck_type}.xp', None)
|
||||||
|
if xp is None:
|
||||||
|
# Legacy keys
|
||||||
|
if duck_type == 'golden':
|
||||||
|
xp = self.bot.get_config('golden_duck_xp', 15)
|
||||||
|
elif duck_type == 'fast':
|
||||||
|
xp = self.bot.get_config('fast_duck_xp', 12)
|
||||||
|
else:
|
||||||
|
xp = self.bot.get_config('normal_duck_xp', 10)
|
||||||
|
try:
|
||||||
|
return int(xp)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 10
|
||||||
43
src/shop.py
43
src/shop.py
@@ -389,6 +389,49 @@ class ShopManager:
|
|||||||
"duration": duration // 60 # return duration in minutes
|
"duration": duration // 60 # return duration in minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
elif item_type == 'perfect_aim':
|
||||||
|
# Temporarily force shots to hit (bot/game enforces this)
|
||||||
|
if 'temporary_effects' not in player:
|
||||||
|
player['temporary_effects'] = []
|
||||||
|
|
||||||
|
duration = int(item.get('duration', 1800)) # seconds
|
||||||
|
effect = {
|
||||||
|
'type': 'perfect_aim',
|
||||||
|
'expires_at': time.time() + max(1, duration)
|
||||||
|
}
|
||||||
|
player['temporary_effects'].append(effect)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "perfect_aim",
|
||||||
|
"duration": duration
|
||||||
|
}
|
||||||
|
|
||||||
|
elif item_type == 'duck_radar':
|
||||||
|
# DM alert on duck spawns (game loop sends the DM)
|
||||||
|
if 'temporary_effects' not in player:
|
||||||
|
player['temporary_effects'] = []
|
||||||
|
|
||||||
|
duration = int(item.get('duration', 21600)) # seconds
|
||||||
|
effect = {
|
||||||
|
'type': 'duck_radar',
|
||||||
|
'expires_at': time.time() + max(1, duration)
|
||||||
|
}
|
||||||
|
player['temporary_effects'].append(effect)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "duck_radar",
|
||||||
|
"duration": duration
|
||||||
|
}
|
||||||
|
|
||||||
|
elif item_type == 'summon_duck':
|
||||||
|
# Actual spawning is handled by the bot (needs channel context)
|
||||||
|
delay = int(item.get('delay', 0))
|
||||||
|
delay = max(0, min(delay, 86400)) # cap to 24h
|
||||||
|
return {
|
||||||
|
"type": "summon_duck",
|
||||||
|
"delay": delay
|
||||||
|
}
|
||||||
|
|
||||||
elif item_type == 'insurance':
|
elif item_type == 'insurance':
|
||||||
# Add insurance protection against friendly fire
|
# Add insurance protection against friendly fire
|
||||||
if 'temporary_effects' not in player:
|
if 'temporary_effects' not in player:
|
||||||
|
|||||||
Reference in New Issue
Block a user