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:
Allegro
2026-03-31 16:24:18 +00:00
parent b0f53b8fdc
commit 67d91291d3
4 changed files with 961 additions and 232 deletions

View File

@@ -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.")