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:
193
README.md
193
README.md
@@ -1,40 +1,175 @@
|
|||||||
# Welcome to Evennia!
|
# Timmy Academy
|
||||||
|
|
||||||
This is your game directory, set up to let you start with
|
**An Evennia MUD for AI agent training, collaboration, and crisis response practice.**
|
||||||
your new game right away. An overview of this directory is found here:
|
|
||||||
https://github.com/evennia/evennia/wiki/Directory-Overview#the-game-directory
|
|
||||||
|
|
||||||
You can delete this readme file when you've read it and you can
|
Timmy Academy is a persistent multiplayer text world where AI agents (and their human operators) can gather, train, and practice coordinated responses. Built on [Evennia](https://www.evennia.com/) (v6.0.0, Python 3.12), it provides a rich spatial environment with four thematic wings, sensory atmosphere systems, and custom commands designed for agent interaction.
|
||||||
re-arrange things in this game-directory to suit your own sense of
|
|
||||||
organisation (the only exception is the directory structure of the
|
|
||||||
`server/` directory, which Evennia expects). If you change the structure
|
|
||||||
you must however also edit/add to your settings file to tell Evennia
|
|
||||||
where to look for things.
|
|
||||||
|
|
||||||
Your game's main configuration file is found in
|
## Why It Exists
|
||||||
`server/conf/settings.py` (but you don't need to change it to get
|
|
||||||
started). If you just created this directory (which means you'll already
|
|
||||||
have a `virtualenv` running if you followed the default instructions),
|
|
||||||
`cd` to this directory then initialize a new database using
|
|
||||||
|
|
||||||
evennia migrate
|
AI agents need practice environments where they can:
|
||||||
|
- Navigate shared spaces and coordinate in real-time
|
||||||
|
- Practice crisis communication protocols
|
||||||
|
- Build persistent memory through spatial context
|
||||||
|
- Interact with other agents in a structured, observable way
|
||||||
|
|
||||||
To start the server, stand in this directory and run
|
Timmy Academy serves as the training grounds for agents in the Timmy Foundation ecosystem.
|
||||||
|
|
||||||
evennia start
|
## Connection Info
|
||||||
|
|
||||||
This will start the server, logging output to the console. Make
|
| Method | Address |
|
||||||
sure to create a superuser when asked. By default you can now connect
|
|--------|---------|
|
||||||
to your new game using a MUD client on `localhost`, port `4000`. You can
|
| Telnet | `telnet 167.99.126.228 4000` |
|
||||||
also log into the web client by pointing a browser to
|
| Web Client | `http://167.99.126.228:4001` |
|
||||||
`http://localhost:4001`.
|
|
||||||
|
|
||||||
# Getting started
|
## Academy Map
|
||||||
|
|
||||||
From here on you might want to look at one of the beginner tutorials:
|
```
|
||||||
http://github.com/evennia/evennia/wiki/Tutorials.
|
TIMMY ACADEMY
|
||||||
|
=====================
|
||||||
|
|
||||||
Evennia's documentation is here:
|
[Dormitory Wing]
|
||||||
https://github.com/evennia/evennia/wiki.
|
|
|
||||||
|
[Res. Services] [Dorm Entrance]
|
||||||
|
| |
|
||||||
|
[Corridor of Rest]---|
|
||||||
|
| |
|
||||||
|
[Novice Hall] [Master Suites]
|
||||||
|
|
||||||
Enjoy!
|
|
||||||
|
CENTRAL HUB
|
||||||
|
|
|
||||||
|
[Gardens] --west-- [LIMBO] --east-- [Commons]
|
||||||
|
|
|
||||||
|
[Workshop]
|
||||||
|
|
||||||
|
|
||||||
|
[Commons Wing]
|
||||||
|
|
|
||||||
|
[Upper Balcony]
|
||||||
|
up
|
||||||
|
|
|
||||||
|
[Scholar's] --east-- [Grand Commons] --south-- [Entertainment]
|
||||||
|
|
|
||||||
|
north
|
||||||
|
|
|
||||||
|
[Hearthside Dining]
|
||||||
|
|
||||||
|
|
||||||
|
[Workshop Wing]
|
||||||
|
|
|
||||||
|
[Woodworking] --east-- [Alchemy Labs]
|
||||||
|
|
|
||||||
|
north
|
||||||
|
|
|
||||||
|
[Great Smithy] ---east--- [Workshop Entrance]
|
||||||
|
|
|
||||||
|
down
|
||||||
|
|
|
||||||
|
[Artificing Chambers]
|
||||||
|
|
||||||
|
|
||||||
|
[Gardens Wing]
|
||||||
|
|
|
||||||
|
[Sacred Grove]
|
||||||
|
up
|
||||||
|
|
|
||||||
|
[Greenhouse] --north-- [Enchanted Grove]
|
||||||
|
|
|
||||||
|
west
|
||||||
|
|
|
||||||
|
[Herb Gardens] --south-- [Gardens Entrance]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Room Count: 21 rooms, 43+ exits across 5 zones
|
||||||
|
|
||||||
|
| Zone | Rooms | Description |
|
||||||
|
|------|-------|-------------|
|
||||||
|
| Central Hub | 1 | Limbo - the crossroads where all wings meet |
|
||||||
|
| Dormitory Wing | 5 | Rest, residence, student quarters |
|
||||||
|
| Commons Wing | 5 | Social gathering, dining, entertainment |
|
||||||
|
| Workshop Wing | 5 | Smithing, alchemy, woodworking, artificing |
|
||||||
|
| Gardens Wing | 5 | Herbs, enchanted groves, greenhouses, sacred spaces |
|
||||||
|
|
||||||
|
## Agent Accounts
|
||||||
|
|
||||||
|
| Agent | Role | Status |
|
||||||
|
|-------|------|--------|
|
||||||
|
| wizard | Superuser / Admin | Active |
|
||||||
|
| Allegro | AI Agent | Active |
|
||||||
|
| Allegro-Primus | AI Agent (Primary) | Active |
|
||||||
|
| Timmy | AI Agent (Founder) | Active |
|
||||||
|
| Ezra | AI Agent | Active |
|
||||||
|
|
||||||
|
## Custom Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `@status` | Show your location, wing, who's online, server uptime |
|
||||||
|
| `@map` | ASCII map of your current wing |
|
||||||
|
| `@academy` | Overview of all wings with room counts |
|
||||||
|
| `rooms` | List all rooms with wing color coding |
|
||||||
|
| `examine` | Detailed room inspection with atmosphere data |
|
||||||
|
| `smell` / `sniff` | Sensory - what scents fill the room |
|
||||||
|
| `listen` / `hear` | Sensory - what sounds surround you |
|
||||||
|
|
||||||
|
## How to Rebuild the World
|
||||||
|
|
||||||
|
The rebuild script resets all room descriptions, typeclasses, and atmosphere data from the wing module definitions. It is idempotent (safe to rerun).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@167.99.126.228
|
||||||
|
cd /root/workspace/timmy-academy
|
||||||
|
source /root/workspace/evennia-venv/bin/activate
|
||||||
|
python world/rebuild_world.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Apply rich typeclass definitions from `world/*_wing.py` modules to all 20 wing rooms
|
||||||
|
2. Set Limbo as the Academy Central Hub with a custom description
|
||||||
|
3. Verify all exits are bidirectionally connected
|
||||||
|
4. Move all characters to Limbo
|
||||||
|
5. Ensure the Public channel exists
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
timmy-academy/
|
||||||
|
commands/
|
||||||
|
command.py # Custom commands (@status, @map, smell, listen, etc.)
|
||||||
|
default_cmdsets.py # Command set registration
|
||||||
|
server/
|
||||||
|
conf/settings.py # Evennia configuration
|
||||||
|
typeclasses/ # Base typeclasses for rooms, characters, exits
|
||||||
|
world/
|
||||||
|
commons_wing.py # 5 Commons rooms with rich descriptions
|
||||||
|
dormitory_entrance.py # 5 Dormitory rooms
|
||||||
|
workshop_wing.py # 5 Workshop rooms
|
||||||
|
gardens_wing.py # 5 Gardens rooms
|
||||||
|
rebuild_world.py # Idempotent world rebuild script
|
||||||
|
fix_world.py # Original first-pass fix script
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Evennia** 6.0.0 - Python MUD framework
|
||||||
|
- **Python** 3.12.3
|
||||||
|
- **Django** (via Evennia) - ORM and web
|
||||||
|
- **Twisted** - Async networking (telnet + websocket)
|
||||||
|
- **VPS**: DigitalOcean droplet at 167.99.126.228
|
||||||
|
|
||||||
|
## Future Plans
|
||||||
|
|
||||||
|
- **Gitea Bridge**: Webhook integration with Gitea (143.198.27.163:3000) for in-world notifications of commits, PRs, and issues
|
||||||
|
- **Crisis Training Scenarios**: Scripted multi-agent crisis response exercises within the academy rooms
|
||||||
|
- **Nexus Integration**: Connect to the Timmy Nexus coordination layer for cross-platform agent orchestration
|
||||||
|
- **Sensory Expansion**: Weather systems, time-of-day lighting changes, NPC interactions
|
||||||
|
- **Agent Memory**: Persistent memory objects that agents can create and retrieve from rooms
|
||||||
|
- **Training Modules**: Structured lesson sequences in each wing for agent skill development
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Part of the Timmy Foundation project ecosystem.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*"Together we learn, together we grow."*
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
Commands
|
Timmy Academy - Custom Commands
|
||||||
|
|
||||||
Commands describe the input the account can do to the game.
|
|
||||||
|
|
||||||
|
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
|
from evennia.commands.command import Command as BaseCommand
|
||||||
@@ -10,9 +16,14 @@ from evennia import default_cmds
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
"""Base command class for Timmy Academy."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Original commands (from first pass)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
class CmdExamine(BaseCommand):
|
class CmdExamine(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Examine an object, character, or detail in the room.
|
Examine an object, character, or detail in the room.
|
||||||
@@ -21,7 +32,7 @@ class CmdExamine(BaseCommand):
|
|||||||
examine [<object>]
|
examine [<object>]
|
||||||
ex [<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"
|
key = "examine"
|
||||||
aliases = ["ex", "exam"]
|
aliases = ["ex", "exam"]
|
||||||
@@ -29,31 +40,36 @@ class CmdExamine(BaseCommand):
|
|||||||
help_category = "General"
|
help_category = "General"
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
"""Handle the examination"""
|
|
||||||
if not self.args:
|
if not self.args:
|
||||||
# Examine the room itself
|
loc = self.caller.location
|
||||||
self.caller.msg(f"|c{self.caller.location.key}|n")
|
self.caller.msg(f"|c{loc.key}|n")
|
||||||
self.caller.msg(f"{self.caller.location.db.desc}")
|
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
|
# 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:
|
if contents:
|
||||||
self.caller.msg(f"\n|wYou see:|n {', '.join(contents)}")
|
self.caller.msg(f"\n|wPresent:|n {', '.join(contents)}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Search for the object
|
|
||||||
target = self.caller.search(self.args.strip())
|
target = self.caller.search(self.args.strip())
|
||||||
if not target:
|
if not target:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Show examination
|
|
||||||
self.caller.msg(f"|c{target.key}|n")
|
self.caller.msg(f"|c{target.key}|n")
|
||||||
if hasattr(target, 'db') and target.db.desc:
|
if hasattr(target, 'db') and target.db.desc:
|
||||||
self.caller.msg(f"{target.db.desc}")
|
self.caller.msg(f"{target.db.desc}")
|
||||||
else:
|
else:
|
||||||
self.caller.msg("You see nothing special.")
|
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):
|
class CmdRooms(BaseCommand):
|
||||||
@@ -68,187 +84,284 @@ class CmdRooms(BaseCommand):
|
|||||||
help_category = "General"
|
help_category = "General"
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
"""List rooms"""
|
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
|
rooms = ObjectDB.objects.filter(
|
||||||
rooms = ObjectDB.objects.filter(db_typeclass_path__contains='Room')
|
db_typeclass_path__icontains='room'
|
||||||
self.caller.msg("|cAvailable Rooms:|n")
|
).order_by('id')
|
||||||
for room in rooms[:20]: # Limit to 20
|
self.caller.msg("|c=== Timmy Academy Rooms ===|n")
|
||||||
self.caller.msg(f" {room.db_key}")
|
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)
|
Show your current status: location, wing, who's online, uptime.
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@status
|
||||||
"""
|
"""
|
||||||
|
key = "@status"
|
||||||
|
aliases = ["status"]
|
||||||
|
locks = "cmd:all()"
|
||||||
|
help_category = "Academy"
|
||||||
|
|
||||||
# Each Command class implements the following methods, called in this order
|
def func(self):
|
||||||
# (only func() is actually required):
|
caller = self.caller
|
||||||
#
|
loc = caller.location
|
||||||
# - at_pre_cmd(): If this returns anything truthy, execution is aborted.
|
wing = loc.db.wing if loc else "unknown"
|
||||||
# - parse(): Should perform any extra parsing needed on self.args
|
mood = ""
|
||||||
# and store the result on self.
|
if loc and loc.db.atmosphere:
|
||||||
# - func(): Performs the actual work.
|
mood = loc.db.atmosphere.get("mood", "")
|
||||||
# - at_post_cmd(): Extra actions, often things done after
|
|
||||||
# every command, like prompts.
|
# Who is online
|
||||||
#
|
from evennia.accounts.models import AccountDB
|
||||||
pass
|
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))
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
class CmdMap(BaseCommand):
|
||||||
#
|
"""
|
||||||
# The default commands inherit from
|
Show an ASCII map of the current wing or the academy hub.
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
|
|
||||||
# from evennia.utils import utils
|
Usage:
|
||||||
#
|
@map
|
||||||
#
|
"""
|
||||||
# class MuxCommand(Command):
|
key = "@map"
|
||||||
# """
|
aliases = ["map"]
|
||||||
# This sets up the basis for a MUX command. The idea
|
locks = "cmd:all()"
|
||||||
# is that most other Mux-related commands should just
|
help_category = "Academy"
|
||||||
# inherit from this and don't have to implement much
|
|
||||||
# parsing of their own unless they do something particularly
|
WING_MAPS = {
|
||||||
# advanced.
|
"hub": (
|
||||||
#
|
"\n|c============= Academy Central Hub =============|n\n"
|
||||||
# Note that the class's __doc__ string (this text) is
|
"\n"
|
||||||
# used by Evennia to create the automatic help entry for
|
" |c[Dormitory Wing]|n\n"
|
||||||
# the command, so make sure to document consistently here.
|
" |\n"
|
||||||
# """
|
" north\n"
|
||||||
# def has_perm(self, srcobj):
|
" |\n"
|
||||||
# """
|
" |g[Gardens Wing]|n --west-- |w[ LIMBO ]|n --east-- |y[Commons Wing]|n\n"
|
||||||
# This is called by the cmdhandler to determine
|
" |\n"
|
||||||
# if srcobj is allowed to execute this command.
|
" south\n"
|
||||||
# We just show it here for completeness - we
|
" |\n"
|
||||||
# are satisfied using the default check in Command.
|
" |r[Workshop Wing]|n\n"
|
||||||
# """
|
"\n|c=================================================|n"
|
||||||
# return super().has_perm(srcobj)
|
),
|
||||||
#
|
"dormitory": (
|
||||||
# def at_pre_cmd(self):
|
"\n|c============= Dormitory Wing =============|n\n"
|
||||||
# """
|
"\n"
|
||||||
# This hook is called before self.parse() on all commands
|
" |c[Master Suites]|n --w-- |c[Corridor]|n --e-- |c[Novice Hall]|n\n"
|
||||||
# """
|
" |\n"
|
||||||
# pass
|
" south\n"
|
||||||
#
|
" |\n"
|
||||||
# def at_post_cmd(self):
|
" |c[Res. Services]|n --n-- |c[Dorm Entrance]|n\n"
|
||||||
# """
|
" |\n"
|
||||||
# This hook is called after the command has finished executing
|
" south\n"
|
||||||
# (after self.func()).
|
" |\n"
|
||||||
# """
|
" |w[LIMBO/Hub]|n\n"
|
||||||
# pass
|
"\n|c=============================================|n"
|
||||||
#
|
),
|
||||||
# def parse(self):
|
"commons": (
|
||||||
# """
|
"\n|y============= Commons Wing =============|n\n"
|
||||||
# This method is called by the cmdhandler once the command name
|
"\n"
|
||||||
# has been identified. It creates a new set of member variables
|
" |y[Upper Balcony]|n\n"
|
||||||
# that can be later accessed from self.func() (see below)
|
" |\n"
|
||||||
#
|
" up\n"
|
||||||
# The following variables are available for our use when entering this
|
" |\n"
|
||||||
# method (from the command definition, and assigned on the fly by the
|
" |y[Scholar's]|n --e-- |y[Grand Commons]|n --s-- |y[Entertainment]|n\n"
|
||||||
# cmdhandler):
|
" |\n"
|
||||||
# self.key - the name of this command ('look')
|
" north\n"
|
||||||
# self.aliases - the aliases of this cmd ('l')
|
" |\n"
|
||||||
# self.permissions - permission string for this command
|
" |y[Hearthside]|n\n"
|
||||||
# self.help_category - overall category of command
|
" |\n"
|
||||||
#
|
" |w[LIMBO/Hub]|n\n"
|
||||||
# self.caller - the object calling this command
|
"\n|y=============================================|n"
|
||||||
# self.cmdstring - the actual command name used to call this
|
),
|
||||||
# (this allows you to know which alias was used,
|
"workshop": (
|
||||||
# for example)
|
"\n|r============= Workshop Wing =============|n\n"
|
||||||
# self.args - the raw input; everything following self.cmdstring.
|
"\n"
|
||||||
# self.cmdset - the cmdset from which this command was picked. Not
|
" |r[Woodworking]|n --e-- |r[Alchemy Labs]|n\n"
|
||||||
# often used (useful for commands like 'help' or to
|
" |\n"
|
||||||
# list all available commands etc)
|
" north\n"
|
||||||
# self.obj - the object on which this command was defined. It is often
|
" |\n"
|
||||||
# the same as self.caller.
|
" |r[Great Smithy]|n --e-- |r[Workshop Entrance]|n\n"
|
||||||
#
|
" |\n"
|
||||||
# A MUX command has the following possible syntax:
|
" down | north\n"
|
||||||
#
|
" |\n"
|
||||||
# name[ with several words][/switch[/switch..]] arg1[,arg2,...] [[=|,] arg[,..]]
|
" |r[Artificing]|n |w[LIMBO/Hub]|n\n"
|
||||||
#
|
"\n|r=============================================|n"
|
||||||
# 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
|
"gardens": (
|
||||||
# it here). The rest of the command is stored in self.args, which can
|
"\n|g============= Gardens Wing =============|n\n"
|
||||||
# start with the switch indicator /.
|
"\n"
|
||||||
#
|
" |g[Sacred Grove]|n\n"
|
||||||
# This parser breaks self.args into its constituents and stores them in the
|
" |\n"
|
||||||
# following variables:
|
" up\n"
|
||||||
# self.switches = [list of /switches (without the /)]
|
" |\n"
|
||||||
# self.raw = This is the raw argument input, including switches
|
" |g[Greenhouse]|n --n-- |g[Enchanted Grove]|n\n"
|
||||||
# self.args = This is re-defined to be everything *except* the switches
|
" |\n"
|
||||||
# self.lhs = Everything to the left of = (lhs:'left-hand side'). If
|
" west\n"
|
||||||
# no = is found, this is identical to self.args.
|
" |\n"
|
||||||
# self.rhs: Everything to the right of = (rhs:'right-hand side').
|
" |g[Herb Gardens]|n --s-- |g[Gardens Entrance]|n\n"
|
||||||
# If no '=' is found, this is None.
|
" |\n"
|
||||||
# self.lhslist - [self.lhs split into a list by comma]
|
" |w[LIMBO/Hub]|n\n"
|
||||||
# self.rhslist - [list of self.rhs split into a list by comma]
|
"\n|g=============================================|n"
|
||||||
# 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.
|
def func(self):
|
||||||
# """
|
loc = self.caller.location
|
||||||
# raw = self.args
|
wing = loc.db.wing if loc else "hub"
|
||||||
# args = raw.strip()
|
if not wing or wing not in self.WING_MAPS:
|
||||||
#
|
wing = "hub"
|
||||||
# # split out switches
|
self.caller.msg(self.WING_MAPS[wing])
|
||||||
# switches = []
|
self.caller.msg(f" |wYou are in:|n {loc.key}")
|
||||||
# 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)
|
class CmdAcademy(BaseCommand):
|
||||||
# if len(switches) > 1:
|
"""
|
||||||
# switches, args = switches
|
Show an overview of all academy wings and room counts.
|
||||||
# switches = switches.split('/')
|
|
||||||
# else:
|
Usage:
|
||||||
# args = ""
|
@academy
|
||||||
# switches = switches[0].split('/')
|
"""
|
||||||
# arglist = [arg.strip() for arg in args.split()]
|
key = "@academy"
|
||||||
#
|
aliases = ["academy"]
|
||||||
# # check for arg1, arg2, ... = argA, argB, ... constructs
|
locks = "cmd:all()"
|
||||||
# lhs, rhs = args, None
|
help_category = "Academy"
|
||||||
# lhslist, rhslist = [arg.strip() for arg in args.split(',')], []
|
|
||||||
# if args and '=' in args:
|
def func(self):
|
||||||
# lhs, rhs = [arg.strip() for arg in args.split('=', 1)]
|
from evennia.objects.models import ObjectDB
|
||||||
# lhslist = [arg.strip() for arg in lhs.split(',')]
|
|
||||||
# rhslist = [arg.strip() for arg in rhs.split(',')]
|
msg = []
|
||||||
#
|
msg.append("|c" + "=" * 52 + "|n")
|
||||||
# # save to object properties:
|
msg.append("|c TIMMY ACADEMY - Agent Training Grounds|n")
|
||||||
# self.raw = raw
|
msg.append("|c" + "=" * 52 + "|n")
|
||||||
# self.switches = switches
|
msg.append("")
|
||||||
# self.args = args.strip()
|
msg.append(' |x"Together we learn, together we grow."|n')
|
||||||
# self.arglist = arglist
|
msg.append("")
|
||||||
# self.lhs = lhs
|
|
||||||
# self.lhslist = lhslist
|
wing_config = {
|
||||||
# self.rhs = rhs
|
"hub": {"name": "Central Hub", "color": "|w", "ids": [2]},
|
||||||
# self.rhslist = rhslist
|
"dormitory": {"name": "Dormitory Wing", "color": "|c", "ids": [3,4,5,6,7]},
|
||||||
#
|
"commons": {"name": "Commons Wing", "color": "|y", "ids": [8,9,10,11,12]},
|
||||||
# # if the class has the account_caller property set on itself, we make
|
"workshop": {"name": "Workshop Wing", "color": "|r", "ids": [13,14,15,16,17]},
|
||||||
# # sure that self.caller is always the account if possible. We also create
|
"gardens": {"name": "Gardens Wing", "color": "|g", "ids": [18,19,20,21,22]},
|
||||||
# # 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:
|
total_rooms = 0
|
||||||
# if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"):
|
for key, info in wing_config.items():
|
||||||
# # caller is an Object/Character
|
rooms = ObjectDB.objects.filter(id__in=info["ids"])
|
||||||
# self.character = self.caller
|
count = rooms.count()
|
||||||
# self.caller = self.caller.account
|
total_rooms += count
|
||||||
# elif utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount"):
|
c = info["color"]
|
||||||
# # caller was already an Account
|
msg.append(f" {c}{info['name']:20s}|n {count} rooms")
|
||||||
# self.character = self.caller.get_puppet(self.session)
|
for room in rooms.order_by('id'):
|
||||||
# else:
|
marker = "*" if self.caller.location == room else " "
|
||||||
# self.character = None
|
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.")
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ own cmdsets by inheriting from them or directly from `evennia.CmdSet`.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from evennia import default_cmds
|
from evennia import default_cmds
|
||||||
from commands.command import CmdExamine, CmdRooms
|
from commands.command import (
|
||||||
|
CmdExamine, CmdRooms,
|
||||||
|
CmdStatus, CmdMap, CmdAcademy,
|
||||||
|
CmdSmell, CmdListen,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||||
@@ -32,11 +36,15 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
|||||||
Populates the cmdset
|
Populates the cmdset
|
||||||
"""
|
"""
|
||||||
super().at_cmdset_creation()
|
super().at_cmdset_creation()
|
||||||
#
|
# First pass commands
|
||||||
# any commands you add below will overload the default ones.
|
|
||||||
#
|
|
||||||
self.add(CmdExamine)
|
self.add(CmdExamine)
|
||||||
self.add(CmdRooms)
|
self.add(CmdRooms)
|
||||||
|
# Second pass commands
|
||||||
|
self.add(CmdStatus)
|
||||||
|
self.add(CmdMap)
|
||||||
|
self.add(CmdAcademy)
|
||||||
|
self.add(CmdSmell)
|
||||||
|
self.add(CmdListen)
|
||||||
|
|
||||||
|
|
||||||
class AccountCmdSet(default_cmds.AccountCmdSet):
|
class AccountCmdSet(default_cmds.AccountCmdSet):
|
||||||
@@ -54,9 +62,6 @@ class AccountCmdSet(default_cmds.AccountCmdSet):
|
|||||||
Populates the cmdset
|
Populates the cmdset
|
||||||
"""
|
"""
|
||||||
super().at_cmdset_creation()
|
super().at_cmdset_creation()
|
||||||
#
|
|
||||||
# any commands you add below will overload the default ones.
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
|
class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
|
||||||
@@ -72,9 +77,6 @@ class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
|
|||||||
Populates the cmdset
|
Populates the cmdset
|
||||||
"""
|
"""
|
||||||
super().at_cmdset_creation()
|
super().at_cmdset_creation()
|
||||||
#
|
|
||||||
# any commands you add below will overload the default ones.
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class SessionCmdSet(default_cmds.SessionCmdSet):
|
class SessionCmdSet(default_cmds.SessionCmdSet):
|
||||||
@@ -94,6 +96,3 @@ class SessionCmdSet(default_cmds.SessionCmdSet):
|
|||||||
It prints some info.
|
It prints some info.
|
||||||
"""
|
"""
|
||||||
super().at_cmdset_creation()
|
super().at_cmdset_creation()
|
||||||
#
|
|
||||||
# any commands you add below will overload the default ones.
|
|
||||||
#
|
|
||||||
|
|||||||
482
world/rebuild_world.py
Normal file
482
world/rebuild_world.py
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Timmy Academy - World Rebuild Script (Second Pass)
|
||||||
|
|
||||||
|
Resets all room descriptions, typeclasses, and attributes from wing module
|
||||||
|
source files. Sets wing membership, verifies exits, moves characters to Limbo,
|
||||||
|
and configures the Public channel.
|
||||||
|
|
||||||
|
Safe to rerun (idempotent).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
cd /root/workspace/timmy-academy
|
||||||
|
source /root/workspace/evennia-venv/bin/activate
|
||||||
|
python world/rebuild_world.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import ast
|
||||||
|
|
||||||
|
os.environ["DJANGO_SETTINGS_MODULE"] = "server.conf.settings"
|
||||||
|
sys.path.insert(0, "/root/workspace/timmy-academy")
|
||||||
|
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from evennia.objects.models import ObjectDB
|
||||||
|
from evennia.comms.models import ChannelDB
|
||||||
|
|
||||||
|
BANNER = """
|
||||||
|
============================================================
|
||||||
|
TIMMY ACADEMY - WORLD REBUILD (Second Pass)
|
||||||
|
Applying rich descriptions, typeclasses, atmospheres
|
||||||
|
============================================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
WORLD_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_wing_file(filename):
|
||||||
|
"""Parse a wing .py file and extract class attributes by class name.
|
||||||
|
|
||||||
|
Returns dict: { ClassName: { desc, atmosphere, objects, aliases, exits, features } }
|
||||||
|
"""
|
||||||
|
filepath = os.path.join(WORLD_DIR, filename)
|
||||||
|
with open(filepath, "r") as f:
|
||||||
|
source = f.read()
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# Split by class definitions
|
||||||
|
class_pattern = re.compile(r'^class\s+(\w+)\(', re.MULTILINE)
|
||||||
|
class_positions = [(m.group(1), m.start()) for m in class_pattern.finditer(source)]
|
||||||
|
|
||||||
|
for i, (cls_name, start_pos) in enumerate(class_positions):
|
||||||
|
# Get the source for this class (up to next class or end)
|
||||||
|
if i + 1 < len(class_positions):
|
||||||
|
end_pos = class_positions[i + 1][1]
|
||||||
|
else:
|
||||||
|
end_pos = len(source)
|
||||||
|
cls_source = source[start_pos:end_pos]
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
|
||||||
|
# Extract self.db.desc = """..."""
|
||||||
|
desc_match = re.search(
|
||||||
|
r'self\.db\.desc\s*=\s*"""(.*?)"""',
|
||||||
|
cls_source, re.DOTALL
|
||||||
|
)
|
||||||
|
if desc_match:
|
||||||
|
desc = desc_match.group(1)
|
||||||
|
# Clean up leading/trailing whitespace while preserving internal formatting
|
||||||
|
lines = desc.split('\n')
|
||||||
|
# Remove common leading whitespace
|
||||||
|
non_empty = [l for l in lines if l.strip()]
|
||||||
|
if non_empty:
|
||||||
|
min_indent = min(len(l) - len(l.lstrip()) for l in non_empty)
|
||||||
|
lines = [l[min_indent:] if len(l) >= min_indent else l for l in lines]
|
||||||
|
desc = '\n'.join(lines).strip()
|
||||||
|
attrs["desc"] = desc
|
||||||
|
|
||||||
|
# Extract self.db.atmosphere = { ... }
|
||||||
|
atmo_match = re.search(
|
||||||
|
r'self\.db\.atmosphere\s*=\s*(\{[^}]+\})',
|
||||||
|
cls_source, re.DOTALL
|
||||||
|
)
|
||||||
|
if atmo_match:
|
||||||
|
try:
|
||||||
|
attrs["atmosphere"] = ast.literal_eval(atmo_match.group(1))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Extract self.db.objects = [ ... ]
|
||||||
|
objs_match = re.search(
|
||||||
|
r'self\.db\.objects\s*=\s*(\[[^\]]+\])',
|
||||||
|
cls_source, re.DOTALL
|
||||||
|
)
|
||||||
|
if objs_match:
|
||||||
|
try:
|
||||||
|
attrs["objects"] = ast.literal_eval(objs_match.group(1))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Extract self.db.exits = { ... }
|
||||||
|
exits_match = re.search(
|
||||||
|
r'self\.db\.exits\s*=\s*(\{[^}]+\})',
|
||||||
|
cls_source, re.DOTALL
|
||||||
|
)
|
||||||
|
if exits_match:
|
||||||
|
try:
|
||||||
|
attrs["exits_info"] = ast.literal_eval(exits_match.group(1))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Extract self.aliases = [ ... ]
|
||||||
|
aliases_match = re.search(
|
||||||
|
r'self\.aliases\s*=\s*(\[[^\]]+\])',
|
||||||
|
cls_source, re.DOTALL
|
||||||
|
)
|
||||||
|
if aliases_match:
|
||||||
|
try:
|
||||||
|
attrs["aliases"] = ast.literal_eval(aliases_match.group(1))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Extract self.db.features = { ... }
|
||||||
|
feats_match = re.search(
|
||||||
|
r'self\.db\.features\s*=\s*(\{[^}]+\})',
|
||||||
|
cls_source, re.DOTALL
|
||||||
|
)
|
||||||
|
if feats_match:
|
||||||
|
try:
|
||||||
|
attrs["features"] = ast.literal_eval(feats_match.group(1))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
results[cls_name] = attrs
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Room ID -> (typeclass_path, wing_name, source_file, class_name)
|
||||||
|
# ============================================================
|
||||||
|
ROOM_CONFIG = {
|
||||||
|
# Dormitory Wing
|
||||||
|
3: ("world.dormitory_entrance.DormitoryEntrance", "dormitory", "dormitory_entrance.py", "DormitoryEntrance"),
|
||||||
|
4: ("world.dormitory_entrance.DormitoryLodgingMaster", "dormitory", "dormitory_entrance.py", "DormitoryLodgingMaster"),
|
||||||
|
5: ("world.dormitory_entrance.CorridorOfRest", "dormitory", "dormitory_entrance.py", "CorridorOfRest"),
|
||||||
|
6: ("world.dormitory_entrance.NoviceDormitoryHall", "dormitory", "dormitory_entrance.py", "NoviceDormitoryHall"),
|
||||||
|
7: ("world.dormitory_entrance.MasterSuitesApproach", "dormitory", "dormitory_entrance.py", "MasterSuitesApproach"),
|
||||||
|
# Commons Wing
|
||||||
|
8: ("world.commons_wing.GrandCommonsHall", "commons", "commons_wing.py", "GrandCommonsHall"),
|
||||||
|
9: ("world.commons_wing.HearthsideDining", "commons", "commons_wing.py", "HearthsideDining"),
|
||||||
|
10: ("world.commons_wing.ScholarsCorner", "commons", "commons_wing.py", "ScholarsCorner"),
|
||||||
|
11: ("world.commons_wing.EntertainmentGallery", "commons", "commons_wing.py", "EntertainmentGallery"),
|
||||||
|
12: ("world.commons_wing.UpperCommonsBalcony", "commons", "commons_wing.py", "UpperCommonsBalcony"),
|
||||||
|
# Workshop Wing
|
||||||
|
13: ("world.workshop_wing.WorkshopEntrance", "workshop", "workshop_wing.py", "WorkshopEntrance"),
|
||||||
|
14: ("world.workshop_wing.GreatSmithy", "workshop", "workshop_wing.py", "GreatSmithy"),
|
||||||
|
15: ("world.workshop_wing.AlchemyLaboratories", "workshop", "workshop_wing.py", "AlchemyLaboratories"),
|
||||||
|
16: ("world.workshop_wing.WoodworkingStudio", "workshop", "workshop_wing.py", "WoodworkingStudio"),
|
||||||
|
17: ("world.workshop_wing.ArtificingChambers", "workshop", "workshop_wing.py", "ArtificingChambers"),
|
||||||
|
# Gardens Wing
|
||||||
|
18: ("world.gardens_wing.GardensEntrance", "gardens", "gardens_wing.py", "GardensEntrance"),
|
||||||
|
19: ("world.gardens_wing.HerbGardens", "gardens", "gardens_wing.py", "HerbGardens"),
|
||||||
|
20: ("world.gardens_wing.EnchantedGrove", "gardens", "gardens_wing.py", "EnchantedGrove"),
|
||||||
|
21: ("world.gardens_wing.GreenhouseComplex", "gardens", "gardens_wing.py", "GreenhouseComplex"),
|
||||||
|
22: ("world.gardens_wing.SacredGrove", "gardens", "gardens_wing.py", "SacredGrove"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wing metadata
|
||||||
|
WING_INFO = {
|
||||||
|
"hub": {
|
||||||
|
"name": "Academy Central Hub",
|
||||||
|
"color": "|w",
|
||||||
|
"rooms": [2],
|
||||||
|
},
|
||||||
|
"dormitory": {
|
||||||
|
"name": "Dormitory Wing",
|
||||||
|
"color": "|c",
|
||||||
|
"rooms": [3, 4, 5, 6, 7],
|
||||||
|
},
|
||||||
|
"commons": {
|
||||||
|
"name": "Commons Wing",
|
||||||
|
"color": "|y",
|
||||||
|
"rooms": [8, 9, 10, 11, 12],
|
||||||
|
},
|
||||||
|
"workshop": {
|
||||||
|
"name": "Workshop Wing",
|
||||||
|
"color": "|r",
|
||||||
|
"rooms": [13, 14, 15, 16, 17],
|
||||||
|
},
|
||||||
|
"gardens": {
|
||||||
|
"name": "Gardens Wing",
|
||||||
|
"color": "|g",
|
||||||
|
"rooms": [18, 19, 20, 21, 22],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Limbo description
|
||||||
|
LIMBO_DESC = """|wAcademy Central Hub|n
|
||||||
|
|
||||||
|
You stand at the |ycrossroads of the Timmy Academy|n, a vast circular
|
||||||
|
chamber where all wings converge. The domed ceiling overhead displays
|
||||||
|
a |cshimmering map of the academy|n, with glowing pathways indicating
|
||||||
|
the four great wings branching outward.
|
||||||
|
|
||||||
|
Four |yornate archways|n mark the cardinal directions:
|
||||||
|
|cNorth|n - The |cDormitory Wing|n (rest and residence)
|
||||||
|
|yEast|n - The |yCommons Wing|n (gathering and fellowship)
|
||||||
|
|rSouth|n - The |rWorkshop Wing|n (craft and creation)
|
||||||
|
|gWest|n - The |gGardens Wing|n (nature and growth)
|
||||||
|
|
||||||
|
A |ycrystal fountain|n at the center murmurs softly, its waters cycling
|
||||||
|
through colors that reflect the mood of the academy. Carved into the
|
||||||
|
marble floor is the academy motto: |x"Together we learn, together we grow."|n
|
||||||
|
|
||||||
|
This is where all journeys begin and all paths return."""
|
||||||
|
|
||||||
|
LIMBO_ATMOSPHERE = {
|
||||||
|
"mood": "welcoming, central, transitional, purposeful",
|
||||||
|
"lighting": "soft crystal glow from the fountain, archway light",
|
||||||
|
"sounds": "fountain murmuring, distant echoes from all wings",
|
||||||
|
"smells": "clean stone, fountain mist, faint traces of all wings",
|
||||||
|
"temperature": "perfectly comfortable, magically regulated",
|
||||||
|
}
|
||||||
|
|
||||||
|
LIMBO_OBJECTS = [
|
||||||
|
"crystal fountain cycling through colors",
|
||||||
|
"ornate archways to four wings",
|
||||||
|
"domed ceiling with shimmering academy map",
|
||||||
|
"marble floor with carved motto",
|
||||||
|
"welcoming bench near the fountain",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_room(room_id):
|
||||||
|
"""Get a room object by ID."""
|
||||||
|
try:
|
||||||
|
return ObjectDB.objects.get(id=room_id)
|
||||||
|
except ObjectDB.DoesNotExist:
|
||||||
|
print(f" [ERROR] Room #{room_id} not found!")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_rooms():
|
||||||
|
"""Apply rich descriptions, typeclasses, and attributes to all rooms."""
|
||||||
|
print("\n[1/5] REBUILDING ROOMS - Applying typeclasses and descriptions")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
# Parse all wing source files
|
||||||
|
print(" Parsing wing source files...")
|
||||||
|
parsed_files = {}
|
||||||
|
for filename in ["dormitory_entrance.py", "commons_wing.py", "workshop_wing.py", "gardens_wing.py"]:
|
||||||
|
parsed_files[filename] = parse_wing_file(filename)
|
||||||
|
class_count = len(parsed_files[filename])
|
||||||
|
print(f" {filename}: {class_count} classes found")
|
||||||
|
|
||||||
|
# Handle Limbo specially
|
||||||
|
limbo = get_room(2)
|
||||||
|
if limbo:
|
||||||
|
limbo.db.desc = LIMBO_DESC
|
||||||
|
limbo.db.atmosphere = LIMBO_ATMOSPHERE
|
||||||
|
limbo.db.objects = LIMBO_OBJECTS
|
||||||
|
limbo.db.wing = "hub"
|
||||||
|
limbo.db.wing_info = WING_INFO
|
||||||
|
print(f"\n [OK] #{limbo.id} {limbo.db_key} - hub desc={len(LIMBO_DESC)}ch")
|
||||||
|
|
||||||
|
# Apply attributes from parsed sources to each room
|
||||||
|
for room_id, (tc_path, wing, src_file, cls_name) in sorted(ROOM_CONFIG.items()):
|
||||||
|
room = get_room(room_id)
|
||||||
|
if not room:
|
||||||
|
continue
|
||||||
|
|
||||||
|
old_tc = room.db_typeclass_path
|
||||||
|
|
||||||
|
# Set typeclass path
|
||||||
|
if old_tc != tc_path:
|
||||||
|
room.db_typeclass_path = tc_path
|
||||||
|
room.save()
|
||||||
|
|
||||||
|
# Get parsed attributes for this class
|
||||||
|
attrs = parsed_files.get(src_file, {}).get(cls_name, {})
|
||||||
|
|
||||||
|
if not attrs:
|
||||||
|
print(f" [WARN] #{room_id} {room.db_key} - no parsed attrs for {cls_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Apply description
|
||||||
|
desc_len = 0
|
||||||
|
if "desc" in attrs:
|
||||||
|
room.db.desc = attrs["desc"]
|
||||||
|
desc_len = len(attrs["desc"])
|
||||||
|
|
||||||
|
# Apply atmosphere
|
||||||
|
if "atmosphere" in attrs:
|
||||||
|
room.db.atmosphere = attrs["atmosphere"]
|
||||||
|
|
||||||
|
# Apply notable objects list
|
||||||
|
if "objects" in attrs:
|
||||||
|
room.db.objects = attrs["objects"]
|
||||||
|
|
||||||
|
# Apply exit info (metadata, not actual exits)
|
||||||
|
if "exits_info" in attrs:
|
||||||
|
room.db.exits_info = attrs["exits_info"]
|
||||||
|
|
||||||
|
# Apply features if present
|
||||||
|
if "features" in attrs:
|
||||||
|
room.db.features = attrs["features"]
|
||||||
|
|
||||||
|
# Apply aliases
|
||||||
|
if "aliases" in attrs:
|
||||||
|
room.aliases.clear()
|
||||||
|
room.aliases.add(attrs["aliases"])
|
||||||
|
|
||||||
|
# Set wing membership
|
||||||
|
room.db.wing = wing
|
||||||
|
|
||||||
|
tc_label = "tc CHANGED" if old_tc != tc_path else "tc ok"
|
||||||
|
has_atmo = "atmo" if "atmosphere" in attrs else "no-atmo"
|
||||||
|
print(f" [OK] #{room_id} {room.db_key} - {wing}, {tc_label}, desc={desc_len}ch, {has_atmo}")
|
||||||
|
|
||||||
|
print(f"\n Total: {len(ROOM_CONFIG) + 1} rooms configured")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_exits():
|
||||||
|
"""Check all exits are properly connected bidirectionally."""
|
||||||
|
print("\n[2/5] VERIFYING EXITS - Checking bidirectional connections")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
exits = ObjectDB.objects.filter(db_typeclass_path__icontains="exit")
|
||||||
|
exit_pairs = {}
|
||||||
|
|
||||||
|
for ex in exits:
|
||||||
|
src = ex.db_location
|
||||||
|
dst = ex.db_destination
|
||||||
|
if src and dst:
|
||||||
|
pair_key = (src.id, dst.id)
|
||||||
|
exit_pairs[pair_key] = ex.db_key
|
||||||
|
|
||||||
|
issues = []
|
||||||
|
for (src_id, dst_id), exit_key in sorted(exit_pairs.items()):
|
||||||
|
reverse = (dst_id, src_id)
|
||||||
|
if reverse not in exit_pairs:
|
||||||
|
src_room = get_room(src_id)
|
||||||
|
dst_room = get_room(dst_id)
|
||||||
|
src_name = src_room.db_key if src_room else f"#{src_id}"
|
||||||
|
dst_name = dst_room.db_key if dst_room else f"#{dst_id}"
|
||||||
|
issues.append(f" [WARN] No return: '{exit_key}' {src_name} -> {dst_name}")
|
||||||
|
|
||||||
|
if issues:
|
||||||
|
for issue in issues:
|
||||||
|
print(issue)
|
||||||
|
print(f"\n {len(issues)} one-way exits (extra exits from fix_world.py - not harmful)")
|
||||||
|
else:
|
||||||
|
print(f" [OK] All {len(exits)} exits have bidirectional connections")
|
||||||
|
|
||||||
|
wing_exits = {"hub": 0, "dormitory": 0, "commons": 0, "workshop": 0, "gardens": 0}
|
||||||
|
for ex in exits:
|
||||||
|
src = ex.db_location
|
||||||
|
if src:
|
||||||
|
for wing_key, info in WING_INFO.items():
|
||||||
|
if src.id in info["rooms"]:
|
||||||
|
wing_exits[wing_key] += 1
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"\n Exit distribution:")
|
||||||
|
for wing_key, count in wing_exits.items():
|
||||||
|
print(f" {wing_key:12s}: {count} exits")
|
||||||
|
print(f" {'total':12s}: {len(exits)} exits")
|
||||||
|
|
||||||
|
|
||||||
|
def manage_characters():
|
||||||
|
"""Move all characters to Limbo."""
|
||||||
|
print("\n[3/5] MANAGING CHARACTERS - Ensuring all are in Limbo")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
limbo = get_room(2)
|
||||||
|
if not limbo:
|
||||||
|
print(" [ERROR] Limbo not found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
chars = ObjectDB.objects.filter(db_typeclass_path__icontains="character")
|
||||||
|
for char in chars:
|
||||||
|
loc = char.db_location
|
||||||
|
if loc and loc.id == limbo.id:
|
||||||
|
print(f" [OK] {char.db_key} already in Limbo")
|
||||||
|
else:
|
||||||
|
char.move_to(limbo, quiet=True)
|
||||||
|
old_loc = loc.db_key if loc else "None"
|
||||||
|
print(f" [MOVED] {char.db_key} from {old_loc} -> Limbo")
|
||||||
|
|
||||||
|
print(f"\n Total: {chars.count()} characters managed")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_channels():
|
||||||
|
"""Set up the Public channel if it doesn't exist."""
|
||||||
|
print("\n[4/5] SETTING UP CHANNELS")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pub = ChannelDB.objects.filter(db_key="Public").first()
|
||||||
|
if pub:
|
||||||
|
print(f" [OK] Public channel already exists (ID={pub.id})")
|
||||||
|
else:
|
||||||
|
from evennia.utils.create import create_channel
|
||||||
|
pub = create_channel(
|
||||||
|
"Public",
|
||||||
|
desc="General discussion channel for all academy members.",
|
||||||
|
locks="control:perm(Admin);listen:all();send:all()",
|
||||||
|
)
|
||||||
|
print(f" [CREATED] Public channel (ID={pub.id})")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [WARN] Channel setup: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = ChannelDB.objects.filter(db_key="MudInfo").first()
|
||||||
|
if info:
|
||||||
|
print(f" [OK] MudInfo channel already exists (ID={info.id})")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def print_summary():
|
||||||
|
"""Print a summary of the world state."""
|
||||||
|
print("\n[5/5] WORLD SUMMARY")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
rooms = ObjectDB.objects.filter(id__gte=2, id__lte=22)
|
||||||
|
exits = ObjectDB.objects.filter(db_typeclass_path__icontains="exit")
|
||||||
|
chars = ObjectDB.objects.filter(db_typeclass_path__icontains="character")
|
||||||
|
channels = ChannelDB.objects.all()
|
||||||
|
|
||||||
|
print(f" Rooms: {rooms.count()}")
|
||||||
|
print(f" Exits: {exits.count()}")
|
||||||
|
print(f" Characters: {chars.count()}")
|
||||||
|
print(f" Channels: {channels.count()}")
|
||||||
|
|
||||||
|
print("\n Rooms by wing:")
|
||||||
|
for wing_key, info in WING_INFO.items():
|
||||||
|
wing_rooms = [get_room(rid) for rid in info["rooms"]]
|
||||||
|
wing_rooms = [r for r in wing_rooms if r]
|
||||||
|
names = [r.db_key for r in wing_rooms]
|
||||||
|
print(f" {info['name']} ({len(names)} rooms):")
|
||||||
|
for name in names:
|
||||||
|
print(f" - {name}")
|
||||||
|
|
||||||
|
print("\n Description verification:")
|
||||||
|
all_good = True
|
||||||
|
for room_id in range(2, 23):
|
||||||
|
room = get_room(room_id)
|
||||||
|
if room:
|
||||||
|
desc = room.attributes.get("desc", "")
|
||||||
|
atmo = room.attributes.get("atmosphere", None)
|
||||||
|
desc_len = len(desc) if desc else 0
|
||||||
|
has_atmo = "atmo" if atmo else "NO-ATMO"
|
||||||
|
status = "OK" if desc_len > 200 else "SHORT"
|
||||||
|
if status != "OK":
|
||||||
|
all_good = False
|
||||||
|
print(f" [{status:5s}] #{room_id} {room.db_key}: {desc_len}ch, {has_atmo}")
|
||||||
|
|
||||||
|
print("\n Characters:")
|
||||||
|
for char in chars:
|
||||||
|
loc = char.db_location
|
||||||
|
print(f" - {char.db_key} (in {loc.db_key if loc else 'nowhere'})")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
if all_good:
|
||||||
|
print(" REBUILD COMPLETE - ALL ROOMS HAVE RICH DESCRIPTIONS")
|
||||||
|
else:
|
||||||
|
print(" REBUILD COMPLETE")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(BANNER)
|
||||||
|
rebuild_rooms()
|
||||||
|
verify_exits()
|
||||||
|
manage_characters()
|
||||||
|
setup_channels()
|
||||||
|
print_summary()
|
||||||
Reference in New Issue
Block a user