build: second pass — rich descriptions, custom commands, README
- world/rebuild_world.py: Comprehensive idempotent rebuild script that parses wing module source files and applies rich multi-paragraph descriptions (800-1361 chars each), atmosphere data (mood, lighting, sounds, smells, temperature), notable objects lists, and room aliases to all 21 rooms. Sets typeclasses, verifies 43 exits, moves all 5 characters to Limbo, and configures Public channel. - commands/command.py: Added 5 new custom commands: @status - Agent status (location, wing, online users, uptime) @map - ASCII map of current wing @academy - Overview of all 4 wings with room counts smell/sniff - Sensory command using room atmosphere data listen/hear - Sensory command using room atmosphere data - commands/default_cmdsets.py: Registered all new commands in the CharacterCmdSet - README.md: Complete rewrite with project description, connection info, ASCII room maps, agent accounts, rebuild instructions, tech stack, and future plans (Gitea bridge, crisis training, Nexus integration)
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
"""
|
||||
Commands
|
||||
|
||||
Commands describe the input the account can do to the game.
|
||||
Timmy Academy - Custom Commands
|
||||
|
||||
Includes:
|
||||
- CmdExamine: Examine objects/rooms in detail
|
||||
- CmdRooms: List available rooms
|
||||
- CmdStatus: Show current agent status
|
||||
- CmdMap: Show ASCII map of current wing
|
||||
- CmdAcademy: Show overview of all 4 wings
|
||||
- CmdSmell: Use atmosphere data for scents
|
||||
- CmdListen: Use atmosphere data for sounds
|
||||
"""
|
||||
|
||||
from evennia.commands.command import Command as BaseCommand
|
||||
@@ -10,9 +16,14 @@ from evennia import default_cmds
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Base command class for Timmy Academy."""
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Original commands (from first pass)
|
||||
# ============================================================
|
||||
|
||||
class CmdExamine(BaseCommand):
|
||||
"""
|
||||
Examine an object, character, or detail in the room.
|
||||
@@ -21,7 +32,7 @@ class CmdExamine(BaseCommand):
|
||||
examine [<object>]
|
||||
ex [<object>]
|
||||
|
||||
If no object is given, examines the current room.
|
||||
If no object is given, examines the current room in detail.
|
||||
"""
|
||||
key = "examine"
|
||||
aliases = ["ex", "exam"]
|
||||
@@ -29,31 +40,36 @@ class CmdExamine(BaseCommand):
|
||||
help_category = "General"
|
||||
|
||||
def func(self):
|
||||
"""Handle the examination"""
|
||||
if not self.args:
|
||||
# Examine the room itself
|
||||
self.caller.msg(f"|c{self.caller.location.key}|n")
|
||||
self.caller.msg(f"{self.caller.location.db.desc}")
|
||||
loc = self.caller.location
|
||||
self.caller.msg(f"|c{loc.key}|n")
|
||||
self.caller.msg(f"{loc.db.desc}")
|
||||
# Show atmosphere if available
|
||||
atmo = loc.db.atmosphere
|
||||
if atmo:
|
||||
self.caller.msg("\n|wAtmosphere:|n")
|
||||
for key, val in atmo.items():
|
||||
self.caller.msg(f" |w{key.capitalize()}:|n {val}")
|
||||
# Show notable objects
|
||||
objs = loc.db.objects
|
||||
if objs:
|
||||
self.caller.msg("\n|wNotable features:|n")
|
||||
for obj in objs:
|
||||
self.caller.msg(f" - {obj}")
|
||||
# Show contents
|
||||
contents = [obj.key for obj in self.caller.location.contents if obj != self.caller]
|
||||
contents = [obj.key for obj in loc.contents if obj != self.caller]
|
||||
if contents:
|
||||
self.caller.msg(f"\n|wYou see:|n {', '.join(contents)}")
|
||||
self.caller.msg(f"\n|wPresent:|n {', '.join(contents)}")
|
||||
return
|
||||
|
||||
# Search for the object
|
||||
target = self.caller.search(self.args.strip())
|
||||
if not target:
|
||||
return
|
||||
|
||||
# Show examination
|
||||
self.caller.msg(f"|c{target.key}|n")
|
||||
if hasattr(target, 'db') and target.db.desc:
|
||||
self.caller.msg(f"{target.db.desc}")
|
||||
else:
|
||||
self.caller.msg("You see nothing special.")
|
||||
|
||||
# Show type
|
||||
self.caller.msg(f"\n|wType:|n {target.db_typeclass_path.split('.')[-1] if target.db_typeclass_path else 'Unknown'}")
|
||||
|
||||
|
||||
class CmdRooms(BaseCommand):
|
||||
@@ -68,187 +84,284 @@ class CmdRooms(BaseCommand):
|
||||
help_category = "General"
|
||||
|
||||
def func(self):
|
||||
"""List rooms"""
|
||||
from evennia.objects.models import ObjectDB
|
||||
|
||||
rooms = ObjectDB.objects.filter(db_typeclass_path__contains='Room')
|
||||
self.caller.msg("|cAvailable Rooms:|n")
|
||||
for room in rooms[:20]: # Limit to 20
|
||||
self.caller.msg(f" {room.db_key}")
|
||||
rooms = ObjectDB.objects.filter(
|
||||
db_typeclass_path__icontains='room'
|
||||
).order_by('id')
|
||||
self.caller.msg("|c=== Timmy Academy Rooms ===|n")
|
||||
for room in rooms:
|
||||
wing = room.attributes.get("wing", "unknown")
|
||||
colors = {
|
||||
"hub": "|w", "dormitory": "|c",
|
||||
"commons": "|y", "workshop": "|r", "gardens": "|g"
|
||||
}
|
||||
c = colors.get(wing, "|n")
|
||||
marker = "*" if self.caller.location == room else " "
|
||||
self.caller.msg(f" {marker} {c}{room.db_key}|n [{wing}]")
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
# ============================================================
|
||||
# New commands (second pass)
|
||||
# ============================================================
|
||||
|
||||
class CmdStatus(BaseCommand):
|
||||
"""
|
||||
Base command (you may see this if a child command had no help text defined)
|
||||
|
||||
Note that the class's `__doc__` string is used by Evennia to create the
|
||||
automatic help entry for the command, so make sure to document consistently
|
||||
here. Without setting one, the parent's docstring will show (like now).
|
||||
Show your current status: location, wing, who's online, uptime.
|
||||
|
||||
Usage:
|
||||
@status
|
||||
"""
|
||||
key = "@status"
|
||||
aliases = ["status"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Academy"
|
||||
|
||||
# Each Command class implements the following methods, called in this order
|
||||
# (only func() is actually required):
|
||||
#
|
||||
# - at_pre_cmd(): If this returns anything truthy, execution is aborted.
|
||||
# - parse(): Should perform any extra parsing needed on self.args
|
||||
# and store the result on self.
|
||||
# - func(): Performs the actual work.
|
||||
# - at_post_cmd(): Extra actions, often things done after
|
||||
# every command, like prompts.
|
||||
#
|
||||
pass
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
loc = caller.location
|
||||
wing = loc.db.wing if loc else "unknown"
|
||||
mood = ""
|
||||
if loc and loc.db.atmosphere:
|
||||
mood = loc.db.atmosphere.get("mood", "")
|
||||
|
||||
# Who is online
|
||||
from evennia.accounts.models import AccountDB
|
||||
online = AccountDB.objects.filter(db_is_connected=True)
|
||||
online_names = [a.db_key for a in online]
|
||||
|
||||
# Uptime
|
||||
try:
|
||||
from evennia import gametime
|
||||
uptime_secs = gametime.uptime()
|
||||
hours = int(uptime_secs // 3600)
|
||||
mins = int((uptime_secs % 3600) // 60)
|
||||
uptime_str = f"{hours}h {mins}m"
|
||||
except Exception:
|
||||
uptime_str = "unknown"
|
||||
|
||||
msg = []
|
||||
msg.append("|c============= Agent Status =============|n")
|
||||
msg.append(f" |wAgent:|n {caller.key}")
|
||||
msg.append(f" |wLocation:|n {loc.key if loc else 'Void'}")
|
||||
msg.append(f" |wWing:|n {wing}")
|
||||
if mood:
|
||||
msg.append(f" |wMood:|n {mood}")
|
||||
msg.append(f" |wOnline:|n {', '.join(online_names) if online_names else 'nobody'} ({len(online_names)})")
|
||||
msg.append(f" |wUptime:|n {uptime_str}")
|
||||
msg.append("|c=========================================|n")
|
||||
self.caller.msg("\n".join(msg))
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
#
|
||||
# The default commands inherit from
|
||||
#
|
||||
# evennia.commands.default.muxcommand.MuxCommand.
|
||||
#
|
||||
# If you want to make sweeping changes to default commands you can
|
||||
# uncomment this copy of the MuxCommand parent and add
|
||||
#
|
||||
# COMMAND_DEFAULT_CLASS = "commands.command.MuxCommand"
|
||||
#
|
||||
# to your settings file. Be warned that the default commands expect
|
||||
# the functionality implemented in the parse() method, so be
|
||||
# careful with what you change.
|
||||
#
|
||||
# -------------------------------------------------------------
|
||||
class CmdMap(BaseCommand):
|
||||
"""
|
||||
Show an ASCII map of the current wing or the academy hub.
|
||||
|
||||
# from evennia.utils import utils
|
||||
#
|
||||
#
|
||||
# class MuxCommand(Command):
|
||||
# """
|
||||
# This sets up the basis for a MUX command. The idea
|
||||
# is that most other Mux-related commands should just
|
||||
# inherit from this and don't have to implement much
|
||||
# parsing of their own unless they do something particularly
|
||||
# advanced.
|
||||
#
|
||||
# Note that the class's __doc__ string (this text) is
|
||||
# used by Evennia to create the automatic help entry for
|
||||
# the command, so make sure to document consistently here.
|
||||
# """
|
||||
# def has_perm(self, srcobj):
|
||||
# """
|
||||
# This is called by the cmdhandler to determine
|
||||
# if srcobj is allowed to execute this command.
|
||||
# We just show it here for completeness - we
|
||||
# are satisfied using the default check in Command.
|
||||
# """
|
||||
# return super().has_perm(srcobj)
|
||||
#
|
||||
# def at_pre_cmd(self):
|
||||
# """
|
||||
# This hook is called before self.parse() on all commands
|
||||
# """
|
||||
# pass
|
||||
#
|
||||
# def at_post_cmd(self):
|
||||
# """
|
||||
# This hook is called after the command has finished executing
|
||||
# (after self.func()).
|
||||
# """
|
||||
# pass
|
||||
#
|
||||
# def parse(self):
|
||||
# """
|
||||
# This method is called by the cmdhandler once the command name
|
||||
# has been identified. It creates a new set of member variables
|
||||
# that can be later accessed from self.func() (see below)
|
||||
#
|
||||
# The following variables are available for our use when entering this
|
||||
# method (from the command definition, and assigned on the fly by the
|
||||
# cmdhandler):
|
||||
# self.key - the name of this command ('look')
|
||||
# self.aliases - the aliases of this cmd ('l')
|
||||
# self.permissions - permission string for this command
|
||||
# self.help_category - overall category of command
|
||||
#
|
||||
# self.caller - the object calling this command
|
||||
# self.cmdstring - the actual command name used to call this
|
||||
# (this allows you to know which alias was used,
|
||||
# for example)
|
||||
# self.args - the raw input; everything following self.cmdstring.
|
||||
# self.cmdset - the cmdset from which this command was picked. Not
|
||||
# often used (useful for commands like 'help' or to
|
||||
# list all available commands etc)
|
||||
# self.obj - the object on which this command was defined. It is often
|
||||
# the same as self.caller.
|
||||
#
|
||||
# A MUX command has the following possible syntax:
|
||||
#
|
||||
# name[ with several words][/switch[/switch..]] arg1[,arg2,...] [[=|,] arg[,..]]
|
||||
#
|
||||
# The 'name[ with several words]' part is already dealt with by the
|
||||
# cmdhandler at this point, and stored in self.cmdname (we don't use
|
||||
# it here). The rest of the command is stored in self.args, which can
|
||||
# start with the switch indicator /.
|
||||
#
|
||||
# This parser breaks self.args into its constituents and stores them in the
|
||||
# following variables:
|
||||
# self.switches = [list of /switches (without the /)]
|
||||
# self.raw = This is the raw argument input, including switches
|
||||
# self.args = This is re-defined to be everything *except* the switches
|
||||
# self.lhs = Everything to the left of = (lhs:'left-hand side'). If
|
||||
# no = is found, this is identical to self.args.
|
||||
# self.rhs: Everything to the right of = (rhs:'right-hand side').
|
||||
# If no '=' is found, this is None.
|
||||
# self.lhslist - [self.lhs split into a list by comma]
|
||||
# self.rhslist - [list of self.rhs split into a list by comma]
|
||||
# self.arglist = [list of space-separated args (stripped, including '=' if it exists)]
|
||||
#
|
||||
# All args and list members are stripped of excess whitespace around the
|
||||
# strings, but case is preserved.
|
||||
# """
|
||||
# raw = self.args
|
||||
# args = raw.strip()
|
||||
#
|
||||
# # split out switches
|
||||
# switches = []
|
||||
# if args and len(args) > 1 and args[0] == "/":
|
||||
# # we have a switch, or a set of switches. These end with a space.
|
||||
# switches = args[1:].split(None, 1)
|
||||
# if len(switches) > 1:
|
||||
# switches, args = switches
|
||||
# switches = switches.split('/')
|
||||
# else:
|
||||
# args = ""
|
||||
# switches = switches[0].split('/')
|
||||
# arglist = [arg.strip() for arg in args.split()]
|
||||
#
|
||||
# # check for arg1, arg2, ... = argA, argB, ... constructs
|
||||
# lhs, rhs = args, None
|
||||
# lhslist, rhslist = [arg.strip() for arg in args.split(',')], []
|
||||
# if args and '=' in args:
|
||||
# lhs, rhs = [arg.strip() for arg in args.split('=', 1)]
|
||||
# lhslist = [arg.strip() for arg in lhs.split(',')]
|
||||
# rhslist = [arg.strip() for arg in rhs.split(',')]
|
||||
#
|
||||
# # save to object properties:
|
||||
# self.raw = raw
|
||||
# self.switches = switches
|
||||
# self.args = args.strip()
|
||||
# self.arglist = arglist
|
||||
# self.lhs = lhs
|
||||
# self.lhslist = lhslist
|
||||
# self.rhs = rhs
|
||||
# self.rhslist = rhslist
|
||||
#
|
||||
# # if the class has the account_caller property set on itself, we make
|
||||
# # sure that self.caller is always the account if possible. We also create
|
||||
# # a special property "character" for the puppeted object, if any. This
|
||||
# # is convenient for commands defined on the Account only.
|
||||
# if hasattr(self, "account_caller") and self.account_caller:
|
||||
# if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"):
|
||||
# # caller is an Object/Character
|
||||
# self.character = self.caller
|
||||
# self.caller = self.caller.account
|
||||
# elif utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount"):
|
||||
# # caller was already an Account
|
||||
# self.character = self.caller.get_puppet(self.session)
|
||||
# else:
|
||||
# self.character = None
|
||||
Usage:
|
||||
@map
|
||||
"""
|
||||
key = "@map"
|
||||
aliases = ["map"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Academy"
|
||||
|
||||
WING_MAPS = {
|
||||
"hub": (
|
||||
"\n|c============= Academy Central Hub =============|n\n"
|
||||
"\n"
|
||||
" |c[Dormitory Wing]|n\n"
|
||||
" |\n"
|
||||
" north\n"
|
||||
" |\n"
|
||||
" |g[Gardens Wing]|n --west-- |w[ LIMBO ]|n --east-- |y[Commons Wing]|n\n"
|
||||
" |\n"
|
||||
" south\n"
|
||||
" |\n"
|
||||
" |r[Workshop Wing]|n\n"
|
||||
"\n|c=================================================|n"
|
||||
),
|
||||
"dormitory": (
|
||||
"\n|c============= Dormitory Wing =============|n\n"
|
||||
"\n"
|
||||
" |c[Master Suites]|n --w-- |c[Corridor]|n --e-- |c[Novice Hall]|n\n"
|
||||
" |\n"
|
||||
" south\n"
|
||||
" |\n"
|
||||
" |c[Res. Services]|n --n-- |c[Dorm Entrance]|n\n"
|
||||
" |\n"
|
||||
" south\n"
|
||||
" |\n"
|
||||
" |w[LIMBO/Hub]|n\n"
|
||||
"\n|c=============================================|n"
|
||||
),
|
||||
"commons": (
|
||||
"\n|y============= Commons Wing =============|n\n"
|
||||
"\n"
|
||||
" |y[Upper Balcony]|n\n"
|
||||
" |\n"
|
||||
" up\n"
|
||||
" |\n"
|
||||
" |y[Scholar's]|n --e-- |y[Grand Commons]|n --s-- |y[Entertainment]|n\n"
|
||||
" |\n"
|
||||
" north\n"
|
||||
" |\n"
|
||||
" |y[Hearthside]|n\n"
|
||||
" |\n"
|
||||
" |w[LIMBO/Hub]|n\n"
|
||||
"\n|y=============================================|n"
|
||||
),
|
||||
"workshop": (
|
||||
"\n|r============= Workshop Wing =============|n\n"
|
||||
"\n"
|
||||
" |r[Woodworking]|n --e-- |r[Alchemy Labs]|n\n"
|
||||
" |\n"
|
||||
" north\n"
|
||||
" |\n"
|
||||
" |r[Great Smithy]|n --e-- |r[Workshop Entrance]|n\n"
|
||||
" |\n"
|
||||
" down | north\n"
|
||||
" |\n"
|
||||
" |r[Artificing]|n |w[LIMBO/Hub]|n\n"
|
||||
"\n|r=============================================|n"
|
||||
),
|
||||
"gardens": (
|
||||
"\n|g============= Gardens Wing =============|n\n"
|
||||
"\n"
|
||||
" |g[Sacred Grove]|n\n"
|
||||
" |\n"
|
||||
" up\n"
|
||||
" |\n"
|
||||
" |g[Greenhouse]|n --n-- |g[Enchanted Grove]|n\n"
|
||||
" |\n"
|
||||
" west\n"
|
||||
" |\n"
|
||||
" |g[Herb Gardens]|n --s-- |g[Gardens Entrance]|n\n"
|
||||
" |\n"
|
||||
" |w[LIMBO/Hub]|n\n"
|
||||
"\n|g=============================================|n"
|
||||
),
|
||||
}
|
||||
|
||||
def func(self):
|
||||
loc = self.caller.location
|
||||
wing = loc.db.wing if loc else "hub"
|
||||
if not wing or wing not in self.WING_MAPS:
|
||||
wing = "hub"
|
||||
self.caller.msg(self.WING_MAPS[wing])
|
||||
self.caller.msg(f" |wYou are in:|n {loc.key}")
|
||||
|
||||
|
||||
class CmdAcademy(BaseCommand):
|
||||
"""
|
||||
Show an overview of all academy wings and room counts.
|
||||
|
||||
Usage:
|
||||
@academy
|
||||
"""
|
||||
key = "@academy"
|
||||
aliases = ["academy"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Academy"
|
||||
|
||||
def func(self):
|
||||
from evennia.objects.models import ObjectDB
|
||||
|
||||
msg = []
|
||||
msg.append("|c" + "=" * 52 + "|n")
|
||||
msg.append("|c TIMMY ACADEMY - Agent Training Grounds|n")
|
||||
msg.append("|c" + "=" * 52 + "|n")
|
||||
msg.append("")
|
||||
msg.append(' |x"Together we learn, together we grow."|n')
|
||||
msg.append("")
|
||||
|
||||
wing_config = {
|
||||
"hub": {"name": "Central Hub", "color": "|w", "ids": [2]},
|
||||
"dormitory": {"name": "Dormitory Wing", "color": "|c", "ids": [3,4,5,6,7]},
|
||||
"commons": {"name": "Commons Wing", "color": "|y", "ids": [8,9,10,11,12]},
|
||||
"workshop": {"name": "Workshop Wing", "color": "|r", "ids": [13,14,15,16,17]},
|
||||
"gardens": {"name": "Gardens Wing", "color": "|g", "ids": [18,19,20,21,22]},
|
||||
}
|
||||
|
||||
total_rooms = 0
|
||||
for key, info in wing_config.items():
|
||||
rooms = ObjectDB.objects.filter(id__in=info["ids"])
|
||||
count = rooms.count()
|
||||
total_rooms += count
|
||||
c = info["color"]
|
||||
msg.append(f" {c}{info['name']:20s}|n {count} rooms")
|
||||
for room in rooms.order_by('id'):
|
||||
marker = "*" if self.caller.location == room else " "
|
||||
msg.append(f" {marker} {c}{room.db_key}|n")
|
||||
|
||||
exits = ObjectDB.objects.filter(db_typeclass_path__icontains="exit")
|
||||
chars = ObjectDB.objects.filter(db_typeclass_path__icontains="character")
|
||||
|
||||
msg.append("")
|
||||
msg.append(f" |wTotal:|n {total_rooms} rooms, {exits.count()} exits, {chars.count()} characters")
|
||||
msg.append("")
|
||||
msg.append(" |wConnect:|n telnet 167.99.126.228 4000")
|
||||
msg.append(" |wWeb:|n http://167.99.126.228:4001")
|
||||
msg.append("|c" + "=" * 52 + "|n")
|
||||
self.caller.msg("\n".join(msg))
|
||||
|
||||
|
||||
class CmdSmell(BaseCommand):
|
||||
"""
|
||||
Use your sense of smell to perceive the room.
|
||||
|
||||
Usage:
|
||||
smell
|
||||
sniff
|
||||
|
||||
Describes the scents and aromas in your current location
|
||||
using the room's atmosphere data.
|
||||
"""
|
||||
key = "smell"
|
||||
aliases = ["sniff"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Academy"
|
||||
|
||||
def func(self):
|
||||
loc = self.caller.location
|
||||
if not loc:
|
||||
self.caller.msg("You smell... nothing. The void has no scent.")
|
||||
return
|
||||
|
||||
atmo = loc.db.atmosphere
|
||||
if atmo and "smells" in atmo:
|
||||
self.caller.msg(f"|wYou breathe deeply in {loc.key}...|n")
|
||||
self.caller.msg(f"\n {atmo['smells']}")
|
||||
if "temperature" in atmo:
|
||||
self.caller.msg(f"\n The air feels: {atmo['temperature']}")
|
||||
else:
|
||||
self.caller.msg("You sniff the air but detect nothing noteworthy.")
|
||||
|
||||
|
||||
class CmdListen(BaseCommand):
|
||||
"""
|
||||
Use your sense of hearing to perceive the room.
|
||||
|
||||
Usage:
|
||||
listen
|
||||
|
||||
Describes the sounds in your current location
|
||||
using the room's atmosphere data.
|
||||
"""
|
||||
key = "listen"
|
||||
aliases = ["hear"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Academy"
|
||||
|
||||
def func(self):
|
||||
loc = self.caller.location
|
||||
if not loc:
|
||||
self.caller.msg("You listen... but silence stretches forever in the void.")
|
||||
return
|
||||
|
||||
atmo = loc.db.atmosphere
|
||||
if atmo and "sounds" in atmo:
|
||||
self.caller.msg(f"|wYou pause and listen in {loc.key}...|n")
|
||||
self.caller.msg(f"\n {atmo['sounds']}")
|
||||
if "mood" in atmo:
|
||||
self.caller.msg(f"\n The mood here is: {atmo['mood']}")
|
||||
else:
|
||||
self.caller.msg("You listen carefully but hear nothing unusual.")
|
||||
|
||||
Reference in New Issue
Block a user