From 8fe6d6b8fa38469768469da9c12780d36301b2eb Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Mon, 17 Feb 2025 13:31:02 -0800 Subject: [PATCH] Create a fishing pole ... and fish to be caught --- typeclasses/fishing.py | 225 +++++++++++++++++++++++++++++++++++ typeclasses/npcs.py | 70 +++++++++++ typeclasses/rooms_weather.py | 2 +- world/version1.ev | 55 +++++++++ 4 files changed, 351 insertions(+), 1 deletion(-) create mode 100755 typeclasses/fishing.py create mode 100755 typeclasses/npcs.py diff --git a/typeclasses/fishing.py b/typeclasses/fishing.py new file mode 100755 index 0000000..e1ac799 --- /dev/null +++ b/typeclasses/fishing.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python + +from evennia import ( + Command, + CmdSet, + TICKER_HANDLER, + syscmdkeys, + create_script, +) +from evennia.prototypes.spawner import spawn + +from enum import Enum +from urllib.request import urlopen + +from typeclasses.objects import Object +from typeclasses.npcs import CarriableNPC +from utils.word_list import routput + +import random +import requests + + +class Fishing(Enum): + "The acceptable states of fishing." + CAST = True + REEL = False + + +class CmdThrowFish(Command): + """ + Throw the fish back in the water. + """ + key = "throw" + + def func(self): + self.obj.do_delete(self.caller) + + +class CmdSetFish(CmdSet): + def at_cmdset_creation(self): + self.add(CmdThrowFish) + + +class CmdCast(Command): + """ + Cast the pole. + """ + key = "cast" + + def func(self): + self.obj.do_cast(self.caller) + + +class CmdReel(Command): + """ + Reel the pole. + """ + key = "reel" + + def func(self): + self.obj.do_reel(self.caller) + + +class CmdSetFishing(CmdSet): + def at_cmdset_creation(self): + self.add(CmdCast) + self.add(CmdReel) + + +class Fish(CarriableNPC): + """ + Everyone wants a fish that tells dad jokes, right!? + """ + # The number of seconds to check for the time for a message: + fish_tick = 3 + + def at_object_creation(self): + self.cmdset.add_default(CmdSetFish) + self.db.name = Fish.get_name() + self.db.spoken = 0 + TICKER_HANDLER.add(interval=self.fish_tick, + callback=self.do_speak) + + def at_heard_say(self, message, from_obj): + """ + A simple listener and response. This makes it easy to change for + subclasses of NPCs reacting differently to says. + + """ + return "say", "What was that? I must have water in my ear." + + def do_speak(self): + """ + Called at a repeatable sequence by the ticker, and + it calls at_say() in order to do a type of monologue. + """ + if self.db.spoken == 1: + self.at_say("Whew! Thanks for removing the sharp hook. Not sure how it got on that.") # Change + elif self.db.spoken == 5: + self.at_say(f"My name's {self.db.name}. What's yours?") + self.db.desc = f"{self.db.name}. {self.db.desc}" + elif self.db.spoken == 10: + self.at_say("Did you say something? I think I have water in my ears, as I can't hear a thing.") + elif self.db.spoken == 20: + self.at_say("So... how're you getting along?") + elif self.db.spoken == 30: + self.at_say("Right, right. Still can't hear a things. Hrm.") + elif self.db.spoken == 60: + self.at_say("Do you know why we fish are so easy to weigh? ") + elif self.db.spoken == 61: + self.at_say("Because we have are own scales.") + elif self.db.spoken == 70: + self.at_say("I suppose you could |bthrow|n me back in the water at any time.") + elif self.db.spoken == 120: + self.at_say("I suppose I should pay you back for helping me out with that hook thing. I guess you know I keep all my money at ... the riverbank.") + elif self.db.spoken == 200: + self.at_say("You know the easiest way to catch a fish, right?") + elif self.db.spoken == 201: + self.at_say("Have someone toss it to you.") + elif self.db.spoken == 205: + self.at_say("Ouch. Tough crowd.") + elif self.db.spoken == 300: + self.at_say("Me and my friends started a musical band.") + elif self.db.spoken == 301: + self.at_say("We all play bass.") + elif self.db.spoken == 302: + self.at_say("Alright guys, I said, drop the instruments. We are singing aquapella.") + elif self.db.spoken == 400: + self.at_say("Did you meet the owners of that new fishing store?") + elif self.db.spoken == 401: + self.at_say("Their names are Rod and Annette.") + elif self.db.spoken == 800: + self.at_say("I'm not that smart.") + elif self.db.spoken == 801: + self.at_say("My friends tell me I'm a dumb bass.") + elif self.db.spoken == 803: + self.at_say("Sorry for all the puns. I feel so GILL-ty.") + + elif self.db.spoken > 1000 and self.db.spoken % (24 * 60 * 60 / self.fish_tick): + self.at_say(get_joke()) + self.db.spoken += 1 + + def do_delete(self, fisher): + """ + A visual way to delete the fish. + """ + if fisher.location == fisher.search("mp06"): + fisher.msg(routput("You [toss|heave|throw] the fish back into the [water|pond].")) + fisher.msg(routput("The fish says, \"Bye for now. If you want to talk again, just drop me a line!\"")) + else: + fisher.msg(routput("You [toss|heave|throw] the fish, and an eagle swoops down and snatches it. I'm sure it will carry the fish back to the pond for you.")) + self.delete() + + def get_name(): + return random.choice([ + "Bennie", "Flipper", "Finegan", "Count Bassie" + ]) + + def get_desc(): + return routput(random.choice([ + "A walleye with big bulbous eyes that clearly doesn't get no respect.", + "A bass with amazing neck confidence giving it a most excellent head bob." + "A rainbow trout missing the [red|yellow|green|blue] from its iridescent stripe.", + "A brown trout colored [red|blue|purple|orange].", + # "A spiny perch", + # "A salmon", + # "A pike", + ])) + + +def get_joke(): + "Fetch a random joke from the internet." + r = requests.get("https://icanhazdadjoke.com/", + headers={'Accept':'text/plain'}) + return r.text + + +class FishingPole(Object): + """ + Can produce a Fish. + """ + failure_msgs = [ + "You reel in an empty line.", + "You didn't catch anything.", + "Caught nothing but a bit of weeds, yeck.", + "Caught nothing, but this sure is enjoyable.", + "Did you catch a boot? Nah, it isn't even that good.", + "Anything better that sitting on the dock of the pond?", + ] + + def at_object_creation(self): + self.cmdset.add_default(CmdSetFishing) + + def do_cast(self, fisher): + if fisher.location != fisher.search("mp06"): + fisher.msg("You can't do that without being near the pond.") + elif fisher.db.fishing == Fishing.CAST: + fisher.msg("You need to reel the line in first.") + else: + fisher.db.fishing = Fishing.CAST + fisher.msg(routput(random.choice([ + "You cast out far into the [water|pond].", + "You cast close to the [dock|shore].", + "You cast off to the [right|left] where you [think you|] see a dark pocket.", + ]))) + + def do_reel(self, fisher): + if fisher.db.fishing != Fishing.CAST: + fisher.msg("You need to |bcast|n before you can reel the line back in.") + else: + fisher.db.fishing = Fishing.REEL + if random.randint(1, 100) < 35: + self.give_fish(fisher) + else: + fisher.msg(random.choice(self.failure_msgs)) + + def give_fish(self, fisher): + fish = spawn({ + "typeclass": self.db.make_class or "typeclasses.fishing.Fish", + "key": "fish", + "aliases": [Fish.get_name()], + "desc": Fish.get_desc(), + })[0] + fish.location = fisher + fisher.msg(f"You caught a fish!") diff --git a/typeclasses/npcs.py b/typeclasses/npcs.py new file mode 100755 index 0000000..2315539 --- /dev/null +++ b/typeclasses/npcs.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +from typeclasses.objects import Object +from utils.word_list import routput + +class NPC(Object): + """ + An NPC is an NPC because it can react to what it _hears_. + To do this, implement 'at_heard_say', a function that + returns a string for a response. + """ + def at_heard_say(self, message, from_obj, + is_say=True, is_whisper=False): + "Override to return a string in response to message." + pass + + def msg(self, text=None, from_obj=None, **kwargs): + "Custom msg() method reacting to say." + # make sure to not repeat what we ourselves said or we'll + # create a loop + if from_obj != self: + is_say = False + is_whisper = False + try: + say_text, is_say = text[0], text[1]['type'] == 'say' + is_whisper = text[1]['type'] == 'whisper' + except Exception: + pass + + try: + # message will be on the form ` says, "say_text"` + # we want to get only say_text without the quotes and any spaces + message = message.split('says, ')[1].strip(' "') + shout, response = self.at_heard_say(message, from_obj, + is_say, is_whisper) + if response != None: + self.at_say(response, just_owner=(shout == "shout")) + except Exception: + pass + + # this is needed if anyone ever puppets this NPC - without it + # you would never get any feedback from the server (not even + # the results of look) + super().msg(text=text, from_obj=from_obj, **kwargs) + + +class CarriableNPC(NPC): + """ + A carriable NPC is like any other NPC, except that since it can + be carried and isn't locked down, it can't hear conversation in a + room. + """ + def at_say(self, message, msg_self=None, msg_location=None, + receivers=None, msg_receivers=None, just_owner=True, + **kwargs): + "Does the best it can to speak out loud." + owner = self.location + + if self.location.is_typeclass("typeclasses.rooms.Room"): + super().at_say(message, msg_self=msg_self, + msg_location=msg_location, + receivers=receivers, + msg_receivers=msg_receivers) + elif just_owner: + owner.msg(f"The {self.name} says, \"{message}\"") + else: + owner.msg( + f"The {self.name}, you are carrying, says, \"{message}\"") + owner.location.msg( + f"The {self.name}, carried by {self.location.name}, says, \"{message}\"", exclude=owner) diff --git a/typeclasses/rooms_weather.py b/typeclasses/rooms_weather.py index 3a2b9a4..01e3227 100755 --- a/typeclasses/rooms_weather.py +++ b/typeclasses/rooms_weather.py @@ -136,7 +136,7 @@ class TimeWeatherRoom(Room): # so as to not have all weather rooms update at the same time. self.db.interval = random.randint(50, 70) TICKER_HANDLER.add( - interval=self.db.interval, callback=self.update_weather, idstring="tutorial" + interval=self.db.interval, callback=self.update_weather ) # this is parsed by the 'tutorial' command on TutorialRooms. self.db.tutorial_info = "This room has a Script running that has it echo a weather-related message at irregular intervals." diff --git a/world/version1.ev b/world/version1.ev index 3c2f030..b8b3b7f 100644 --- a/world/version1.ev +++ b/world/version1.ev @@ -429,6 +429,61 @@ Looks good for being out in the weather. +# With a description: + +# [[file:../../../projects/mud.org::*Fishing Pole][Fishing Pole:2]] +@desc pole = A nice pole for catching fish. It even has a hook, and is ready to go! +# Fishing Pole:2 ends here + + + +# What about some details: + +# [[file:../../../projects/mud.org::*Fishing Pole][Fishing Pole:3]] +@detail hook;bait = One of those shiny lures, made from gold coins. Curiouser. +# +@detail water;waves = Despite the weather, the water looks nice...well, nice for fishing. +# +@detail dock = Sturdy and well made. Bobs a little with the waves. +# Fishing Pole:3 ends here + + + +# Need to make the fishing pole “stay” at the Dock. Maybe with a message about sticking around for the next person. + + +# [[file:../../../projects/mud.org::*Fishing Pole][Fishing Pole:4]] +@create/drop sign:typeclasses.readables.Readable +# Fishing Pole:4 ends here + + + +# Should the description also be the message? + +# [[file:../../../projects/mud.org::*Fishing Pole][Fishing Pole:5]] +@desc sign = You see a wood sign tied with a rope around the back of the chair. It reads, |wFish at your own annoyance. Please return pole when finished.|n +# Fishing Pole:5 ends here + + + +# Might as well allow the user to read it: + +# [[file:../../../projects/mud.org::*Fishing Pole][Fishing Pole:6]] +@set sign/inside = "Fish at your own annoyance. Please return pole when finished." +# Fishing Pole:6 ends here + + + +# And lock down the sign: + +# [[file:../../../projects/mud.org::*Fishing Pole][Fishing Pole:7]] +@lock sign = get:false() +# +@set sign/get_err_msg = "This granny knot holding the sign to the chair is serious. You can't take it." +# Fishing Pole:7 ends here + + + # Return to the forest: # [[file:../../../projects/mud.org::*Forest Path][Forest Path:1]]