Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions PokemonLibrary/battle/battle_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ def trigger_wild_encounter(
"""
Trigger a wild Pokemon battle at the current location.

If ``game_state.game_data["_fishing_encounter"]`` is set (by the fishing
mechanic) it is consumed and used as the forced wild species/level instead
of picking randomly from the location's wild pool.

Args:
game_state: The game state
output: The RichLog widget to write to
Expand All @@ -94,10 +98,16 @@ def trigger_wild_encounter(
output.write("")
return

# Pick a random wild species and level
wild_species = random.choice(location.wild_pokemon)
min_lvl, max_lvl = location.wild_level_range
wild_level = random.randint(min_lvl, max_lvl)
# Check for a forced fishing encounter
fishing_enc = game_state.game_data.pop("_fishing_encounter", None)
if fishing_enc:
wild_species = fishing_enc["species"]
wild_level = fishing_enc["level"]
else:
# Pick a random wild species and level
wild_species = random.choice(location.wild_pokemon)
min_lvl, max_lvl = location.wild_level_range
wild_level = random.randint(min_lvl, max_lvl)

# Create and start battle
bs = BattleState()
Expand Down Expand Up @@ -433,6 +443,7 @@ def attempt_catch_pokemon(
show_battle_options_callback,
end_battle_callback,
handle_pokemon_fainted_callback,
ball_type: str = "Pokeball",
) -> None:
"""
Attempt to catch the wild Pokemon with a Pokeball.
Expand All @@ -444,6 +455,7 @@ def attempt_catch_pokemon(
show_battle_options_callback: Callback to show battle options
end_battle_callback: Callback to end battle
handle_pokemon_fainted_callback: Callback to handle Pokemon fainted
ball_type: Type of ball to use (Pokeball, Great Ball, Ultra Ball, Master Ball)
"""
battle = game_state.battle_state

Expand All @@ -459,30 +471,35 @@ def attempt_catch_pokemon(
return

items = game_state.game_data.get("items", {})
pokeballs = items.get("Pokeball", 0)

if pokeballs <= 0:
# Check inventory for the chosen ball type; fall back to any available ball
if items.get(ball_type, 0) <= 0:
# Try to find any available ball to inform the player
output.write("")
output.write("[red]❌ You have no Pokeballs![/red]")
output.write("[dim]Buy Pokeballs at the Pokemart[/dim]")
if ball_type == "Pokeball":
output.write("[red]❌ You have no Pokeballs![/red]")
output.write("[dim]Buy Pokeballs at the Pokemart[/dim]")
else:
output.write(f"[red]❌ You have no {ball_type}s![/red]")
output.write("[dim]Check your bag for available Pokéballs[/dim]")
output.write("")
show_battle_options_callback(output)
pending_command_callback("battle")
return

wild = battle.wild_pokemon

# Use a Pokeball
items["Pokeball"] -= 1
if items["Pokeball"] <= 0:
del items["Pokeball"]
# Consume the ball
items[ball_type] -= 1
if items[ball_type] <= 0:
del items[ball_type]

output.write("")
output.write(f"[bold cyan]You threw a Pokeball at wild {wild['name']}![/bold cyan]")
output.write(f"[bold cyan]You threw a {ball_type} at wild {wild['name']}![/bold cyan]")
output.write("")

# Attempt catch
caught, shakes, messages = battle.attempt_catch("Pokeball")
caught, shakes, messages = battle.attempt_catch(ball_type)

# Show custom messages from catch attempt
for msg in messages:
Expand Down
40 changes: 40 additions & 0 deletions PokemonLibrary/data/move_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ def get(self, key: str, default=None):
effect="raise_defense_2",
effect_chance=100,
),
"BIDE": MoveData(
name="BIDE",
type=NORMAL,
category="physical",
power=1,
accuracy=0,
pp=10,
effect="bide",
effect_chance=100,
),
"BIND": MoveData(
name="BIND",
type=NORMAL,
Expand Down Expand Up @@ -397,6 +407,16 @@ def get(self, key: str, default=None):
effect=None,
effect_chance=None,
),
"DREAM EATER": MoveData(
name="DREAM EATER",
type=PSYCHIC,
category="special",
power=100,
accuracy=100,
pp=15,
effect="dream_eater",
effect_chance=100,
),
"DRILL PECK": MoveData(
name="DRILL PECK",
type=FLYING,
Expand Down Expand Up @@ -1367,6 +1387,16 @@ def get(self, key: str, default=None):
effect="paralysis",
effect_chance=100,
),
"SUBMISSION": MoveData(
name="SUBMISSION",
type=FIGHTING,
category="physical",
power=80,
accuracy=80,
pp=25,
effect="recoil",
effect_chance=100,
),
"SUBSTITUTE": MoveData(
name="SUBSTITUTE",
type=NORMAL,
Expand Down Expand Up @@ -1527,6 +1557,16 @@ def get(self, key: str, default=None):
effect="bad_poison",
effect_chance=100,
),
"TRI ATTACK": MoveData(
name="TRI ATTACK",
type=NORMAL,
category="special",
power=80,
accuracy=100,
pp=10,
effect=None,
effect_chance=None,
),
"TRANSFORM": MoveData(
name="TRANSFORM",
type=NORMAL,
Expand Down
6 changes: 6 additions & 0 deletions PokemonLibrary/exploration.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ def move_to_location(
if "cascade_badge" in badges:
exit_data = {**exit_data, "blocked": False}

# Special check: Unblock Route 21 from Pallet Town once player has used Surf
if matching_exit == "Route 21" and current.name == "Pallet Town":
surf_unlocked = game_state.game_data.get("surf_unlocked", [])
if "Route 21" in surf_unlocked or game_state.cheat_mode:
exit_data = {**exit_data, "blocked": False}

if exit_data.get("blocked", False) and not game_state.cheat_mode:
reason = exit_data.get("reason", "This path is blocked")
output.write("")
Expand Down
208 changes: 208 additions & 0 deletions PokemonLibrary/fishing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
"""
Fishing mechanic for the Pokemon game.

Players can fish for water Pokemon using Old Rod, Good Rod, or Super Rod
at locations that have water access. Each rod type yields different
Pokemon species and level ranges.
"""

from __future__ import annotations

import random
from typing import TYPE_CHECKING

from textual.widgets import RichLog

if TYPE_CHECKING:
from .game_state import GameState

# ---------------------------------------------------------------------------
# Fishing data — rod → (species, weight) tables and level ranges
# ---------------------------------------------------------------------------

# Each entry: (pokemon_name, relative_weight)
# Higher weight = more common catch.
_ROD_TABLES: dict[str, list[tuple[str, int]]] = {
"Old Rod": [
("MAGIKARP", 90),
("GOLDEEN", 10),
],
"Good Rod": [
("MAGIKARP", 60),
("GOLDEEN", 30),
("TENTACOOL", 10),
],
"Super Rod": [
("MAGIKARP", 40),
("GOLDEEN", 35),
("TENTACOOL", 20),
("GYARADOS", 5),
],
}

_ROD_LEVEL_RANGES: dict[str, tuple[int, int]] = {
"Old Rod": (5, 15),
"Good Rod": (10, 30),
"Super Rod": (15, 40),
}

# Locations that have water access and support fishing.
# Any town/route near a body of water is fishable.
_FISHING_LOCATIONS: set[str] = {
"Pallet Town",
"Viridian City",
"Cerulean City",
"Route 4",
"Route 6",
"Route 10",
"Route 12",
"Route 13",
"Route 17",
"Route 19",
"Route 20",
"Route 21",
"Route 24",
"Vermillion City",
"Celadon City",
"Fuchsia City",
"Cinnabar Island",
"Route 25",
}

# Base nibble probability per rod (probability that a Pokemon bites)
_NIBBLE_CHANCE: dict[str, float] = {
"Old Rod": 0.70,
"Good Rod": 0.80,
"Super Rod": 0.90,
}


def get_best_rod(game_state: GameState) -> str | None:
"""
Return the best fishing rod in the player's bag, or None if they have none.

Args:
game_state: Current game state.

Returns:
Canonical name of the best rod available, or None.
"""
items = game_state.game_data.get("items", {})
for rod in ("Super Rod", "Good Rod", "Old Rod"):
if items.get(rod, 0) > 0:
return rod
return None


def get_rod_for_name(game_state: GameState, rod_hint: str) -> str | None:
"""
Return the canonical rod name matching *rod_hint* if the player owns it.

Args:
game_state: Current game state.
rod_hint: Partial or full rod name supplied by the player.

Returns:
Canonical rod name, or None if not found / not owned.
"""
items = game_state.game_data.get("items", {})
hint_lower = rod_hint.lower()
for rod in ("Super Rod", "Good Rod", "Old Rod"):
if rod.lower() == hint_lower or hint_lower in rod.lower():
if items.get(rod, 0) > 0:
return rod
return None


def start_fishing(
game_state: GameState,
output: RichLog,
trigger_wild_callback,
rod_name: str | None = None,
) -> None:
"""
Begin a fishing attempt at the current location.

Selects the appropriate rod, checks that the location supports fishing,
rolls for a nibble, then triggers a wild Pokemon encounter with a water-
type Pokemon if successful.

Args:
game_state: Current game state.
output: RichLog widget.
trigger_wild_callback: Callback(output, forced_pokemon) to start a battle
with a pre-selected wild Pokemon. Signature matches
the ``trigger_wild_encounter`` method on the terminal.
rod_name: Optional rod name specified by the player. When None,
the best available rod is used.
"""
location = game_state.current_location
if not location:
output.write("[red]❌ No current location![/red]")
return

# --- Party check ---
party = game_state.game_data.get("pokemon", [])
if not party:
output.write("")
output.write("[yellow]⚠ You can't fish without Pokemon![/yellow]")
output.write("[dim]Get a starter from Professor Oak first.[/dim]")
output.write("")
return

# --- Location check ---
if location.name not in _FISHING_LOCATIONS:
output.write("")
output.write("[yellow]⚠ You can't fish here![/yellow]")
output.write("[dim]Fish near water routes, lakes, or coastal areas.[/dim]")
output.write("")
return

# --- Rod selection ---
if rod_name:
chosen_rod = get_rod_for_name(game_state, rod_name)
if not chosen_rod:
output.write("")
output.write(f"[red]❌ You don't have a {rod_name}![/red]")
output.write("[dim]Buy fishing rods from the Fishing Guru.[/dim]")
output.write("")
return
else:
chosen_rod = get_best_rod(game_state)
if not chosen_rod:
output.write("")
output.write("[yellow]⚠ You don't have a fishing rod![/yellow]")
output.write("[dim]Obtain a fishing rod to catch water Pokemon.[/dim]")
output.write("[dim] • Old Rod — basic, found early in the game[/dim]")
output.write("[dim] • Good Rod — better variety[/dim]")
output.write("[dim] • Super Rod — rare catches[/dim]")
output.write("")
return

# --- Casting ---
output.write("")
output.write(f"[bold cyan]🎣 You cast your {chosen_rod} into the water...[/bold cyan]")
output.write("")

nibble_chance = _NIBBLE_CHANCE.get(chosen_rod, 0.75)
if random.random() > nibble_chance:
output.write("[dim]...Nothing. The water is still.[/dim]")
output.write("[dim] Try again![/dim]")
output.write("")
return

# --- Pick a Pokemon from the rod's table ---
table = _ROD_TABLES.get(chosen_rod, _ROD_TABLES["Old Rod"])
species_pool = [name for name, _ in table]
weights = [w for _, w in table]
species = random.choices(species_pool, weights=weights, k=1)[0]

level_min, level_max = _ROD_LEVEL_RANGES.get(chosen_rod, (5, 15))
level = random.randint(level_min, level_max)

output.write("[bold green]Oh! A bite![/bold green]")
output.write(f"[green] You reeled in a wild {species}![/green]")
output.write("")

# --- Trigger encounter ---
trigger_wild_callback(output, species, level)
Loading
Loading