rom24-quickmud-python 1.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mud/__init__.py +0 -0
- mud/__main__.py +40 -0
- mud/account/__init__.py +20 -0
- mud/account/account_manager.py +62 -0
- mud/account/account_service.py +80 -0
- mud/advancement.py +62 -0
- mud/affects/saves.py +123 -0
- mud/agent/__init__.py +0 -0
- mud/agent/agent_protocol.py +19 -0
- mud/agent/character_agent.py +61 -0
- mud/combat/__init__.py +3 -0
- mud/combat/engine.py +189 -0
- mud/commands/__init__.py +3 -0
- mud/commands/admin_commands.py +77 -0
- mud/commands/advancement.py +36 -0
- mud/commands/alias_cmds.py +44 -0
- mud/commands/build.py +18 -0
- mud/commands/combat.py +16 -0
- mud/commands/communication.py +55 -0
- mud/commands/decorators.py +11 -0
- mud/commands/dispatcher.py +206 -0
- mud/commands/healer.py +81 -0
- mud/commands/help.py +14 -0
- mud/commands/imc.py +19 -0
- mud/commands/inspection.py +113 -0
- mud/commands/inventory.py +42 -0
- mud/commands/movement.py +71 -0
- mud/commands/notes.py +44 -0
- mud/commands/shop.py +138 -0
- mud/commands/socials.py +34 -0
- mud/config.py +59 -0
- mud/db/__init__.py +0 -0
- mud/db/init.py +27 -0
- mud/db/migrate_from_files.py +87 -0
- mud/db/migrations.py +7 -0
- mud/db/models.py +98 -0
- mud/db/seed.py +28 -0
- mud/db/session.py +11 -0
- mud/devtools/__init__.py +0 -0
- mud/devtools/agent_demo.py +19 -0
- mud/entrypoint.py +34 -0
- mud/game_loop.py +117 -0
- mud/imc/__init__.py +17 -0
- mud/imc/protocol.py +32 -0
- mud/loaders/__init__.py +27 -0
- mud/loaders/area_loader.py +73 -0
- mud/loaders/base_loader.py +38 -0
- mud/loaders/help_loader.py +17 -0
- mud/loaders/json_area_loader.py +203 -0
- mud/loaders/json_loader.py +285 -0
- mud/loaders/mob_loader.py +104 -0
- mud/loaders/obj_loader.py +76 -0
- mud/loaders/reset_loader.py +29 -0
- mud/loaders/room_loader.py +63 -0
- mud/loaders/shop_loader.py +41 -0
- mud/loaders/social_loader.py +16 -0
- mud/loaders/specials_loader.py +63 -0
- mud/logging/__init__.py +0 -0
- mud/logging/admin.py +40 -0
- mud/logging/agent_trace.py +9 -0
- mud/math/c_compat.py +27 -0
- mud/mobprog.py +72 -0
- mud/models/__init__.py +106 -0
- mud/models/area.py +33 -0
- mud/models/area_json.py +27 -0
- mud/models/board.py +49 -0
- mud/models/board_json.py +16 -0
- mud/models/character.py +195 -0
- mud/models/character_json.py +46 -0
- mud/models/constants.py +423 -0
- mud/models/conversion.py +45 -0
- mud/models/help.py +28 -0
- mud/models/help_json.py +14 -0
- mud/models/json_io.py +64 -0
- mud/models/mob.py +82 -0
- mud/models/note.py +29 -0
- mud/models/note_json.py +16 -0
- mud/models/obj.py +82 -0
- mud/models/object.py +28 -0
- mud/models/object_json.py +40 -0
- mud/models/player_json.py +29 -0
- mud/models/room.py +86 -0
- mud/models/room_json.py +46 -0
- mud/models/shop.py +21 -0
- mud/models/shop_json.py +17 -0
- mud/models/skill.py +25 -0
- mud/models/skill_json.py +20 -0
- mud/models/social.py +78 -0
- mud/models/social_json.py +20 -0
- mud/net/__init__.py +9 -0
- mud/net/ansi.py +27 -0
- mud/net/connection.py +174 -0
- mud/net/protocol.py +57 -0
- mud/net/session.py +21 -0
- mud/net/telnet_server.py +31 -0
- mud/network/__init__.py +0 -0
- mud/network/websocket_server.py +83 -0
- mud/network/websocket_session.py +21 -0
- mud/notes.py +45 -0
- mud/persistence.py +185 -0
- mud/registry.py +5 -0
- mud/scripts/convert_are_to_json.py +162 -0
- mud/scripts/convert_help_are_to_json.py +92 -0
- mud/scripts/convert_player_to_json.py +112 -0
- mud/scripts/convert_shops_to_json.py +64 -0
- mud/scripts/convert_skills_to_json.py +166 -0
- mud/scripts/convert_social_are_to_json.py +92 -0
- mud/scripts/load_test_data.py +17 -0
- mud/security/__init__.py +0 -0
- mud/security/bans.py +112 -0
- mud/security/hash_utils.py +20 -0
- mud/server.py +8 -0
- mud/skills/__init__.py +3 -0
- mud/skills/handlers.py +795 -0
- mud/skills/registry.py +97 -0
- mud/spawning/__init__.py +1 -0
- mud/spawning/mob_spawner.py +13 -0
- mud/spawning/obj_spawner.py +18 -0
- mud/spawning/reset_handler.py +222 -0
- mud/spawning/templates.py +63 -0
- mud/spec_funs.py +57 -0
- mud/time.py +48 -0
- mud/utils/rng_mm.py +123 -0
- mud/wiznet.py +74 -0
- mud/world/__init__.py +11 -0
- mud/world/linking.py +31 -0
- mud/world/look.py +29 -0
- mud/world/movement.py +135 -0
- mud/world/world_state.py +179 -0
- rom24_quickmud_python-1.2.2.dist-info/METADATA +236 -0
- rom24_quickmud_python-1.2.2.dist-info/RECORD +135 -0
- rom24_quickmud_python-1.2.2.dist-info/WHEEL +5 -0
- rom24_quickmud_python-1.2.2.dist-info/entry_points.txt +2 -0
- rom24_quickmud_python-1.2.2.dist-info/licenses/LICENSE +21 -0
- rom24_quickmud_python-1.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from mud.models.character import Character
|
|
2
|
+
from mud.registry import room_registry
|
|
3
|
+
from mud.spawning.mob_spawner import spawn_mob
|
|
4
|
+
from mud.net.session import SESSIONS
|
|
5
|
+
from mud.security import bans
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def cmd_who(char: Character, args: str) -> str:
|
|
9
|
+
lines = ["Online Players:"]
|
|
10
|
+
for sess in SESSIONS.values():
|
|
11
|
+
c = sess.character
|
|
12
|
+
room_vnum = c.room.vnum if getattr(c, "room", None) else "?"
|
|
13
|
+
lines.append(f" - {c.name} in room {room_vnum}")
|
|
14
|
+
return "\n".join(lines)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_teleport(char: Character, args: str) -> str:
|
|
18
|
+
if not args.isdigit() or int(args) not in room_registry:
|
|
19
|
+
return "Invalid room."
|
|
20
|
+
target = room_registry[int(args)]
|
|
21
|
+
if char.room:
|
|
22
|
+
char.room.remove_character(char)
|
|
23
|
+
target.add_character(char)
|
|
24
|
+
return f"Teleported to room {args}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cmd_spawn(char: Character, args: str) -> str:
|
|
28
|
+
if not args.isdigit():
|
|
29
|
+
return "Invalid vnum."
|
|
30
|
+
mob = spawn_mob(int(args))
|
|
31
|
+
if not mob:
|
|
32
|
+
return "NPC not found."
|
|
33
|
+
if not char.room:
|
|
34
|
+
return "Nowhere to spawn."
|
|
35
|
+
char.room.add_mob(mob)
|
|
36
|
+
return f"Spawned {mob.name}."
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_ban(char: Character, args: str) -> str:
|
|
40
|
+
host = args.strip()
|
|
41
|
+
if not host:
|
|
42
|
+
return "Usage: ban <host>"
|
|
43
|
+
bans.add_banned_host(host)
|
|
44
|
+
try:
|
|
45
|
+
bans.save_bans_file()
|
|
46
|
+
except Exception:
|
|
47
|
+
# Persistence errors shouldn't block the action in tests
|
|
48
|
+
pass
|
|
49
|
+
return f"Banned {host}."
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cmd_unban(char: Character, args: str) -> str:
|
|
53
|
+
host = args.strip()
|
|
54
|
+
if not host:
|
|
55
|
+
return "Usage: unban <host>"
|
|
56
|
+
if not bans.is_host_banned(host):
|
|
57
|
+
return "Site is not banned."
|
|
58
|
+
bans.remove_banned_host(host)
|
|
59
|
+
try:
|
|
60
|
+
bans.save_bans_file()
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
return f"Unbanned {host}."
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmd_banlist(char: Character, args: str) -> str:
|
|
67
|
+
banned = sorted(list({h for h in list_hosts() for h in [h]}))
|
|
68
|
+
if not banned:
|
|
69
|
+
return "No sites banned."
|
|
70
|
+
lines = ["Banned sites:"] + [f" - {h}" for h in banned]
|
|
71
|
+
return "\n".join(lines)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def list_hosts() -> list[str]:
|
|
75
|
+
# internal helper to read via saving/loading outward if needed later
|
|
76
|
+
# currently directly exposes in-memory set
|
|
77
|
+
return sorted({*bans._banned_hosts}) # type: ignore[attr-defined]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from mud.models.character import Character
|
|
2
|
+
from mud.skills.registry import skill_registry
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def do_practice(char: Character, args: str) -> str:
|
|
6
|
+
if not args:
|
|
7
|
+
return f"You have {char.practice} practice sessions left."
|
|
8
|
+
if char.practice <= 0:
|
|
9
|
+
return "You have no practice sessions left."
|
|
10
|
+
skill_name = args.lower()
|
|
11
|
+
if skill_name not in skill_registry.skills:
|
|
12
|
+
return "You can't practice that."
|
|
13
|
+
current = char.skills.get(skill_name, 0)
|
|
14
|
+
if current >= 75:
|
|
15
|
+
return f"You are already learned at {skill_name}."
|
|
16
|
+
char.practice -= 1
|
|
17
|
+
char.skills[skill_name] = min(current + 25, 75)
|
|
18
|
+
return f"You practice {skill_name}."
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def do_train(char: Character, args: str) -> str:
|
|
22
|
+
if not args:
|
|
23
|
+
return f"You have {char.train} training sessions left."
|
|
24
|
+
if char.train <= 0:
|
|
25
|
+
return "You have no training sessions left."
|
|
26
|
+
stat = args.lower()
|
|
27
|
+
if stat not in {"hp", "mana", "move"}:
|
|
28
|
+
return "Train what?"
|
|
29
|
+
if stat == "hp":
|
|
30
|
+
char.max_hit += 10
|
|
31
|
+
elif stat == "mana":
|
|
32
|
+
char.max_mana += 10
|
|
33
|
+
else:
|
|
34
|
+
char.max_move += 10
|
|
35
|
+
char.train -= 1
|
|
36
|
+
return f"You train your {stat}."
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from mud.models.character import Character
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _format_aliases(aliases: Dict[str, str]) -> str:
|
|
9
|
+
if not aliases:
|
|
10
|
+
return "No aliases defined."
|
|
11
|
+
parts = [f"{k} -> {v}" for k, v in sorted(aliases.items())]
|
|
12
|
+
return "Aliases: " + ", ".join(parts)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def do_alias(char: Character, args: str = "") -> str:
|
|
16
|
+
"""Create or list aliases.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
alias # list
|
|
20
|
+
alias <name> <exp> # set/replace
|
|
21
|
+
"""
|
|
22
|
+
if not args.strip():
|
|
23
|
+
return _format_aliases(char.aliases)
|
|
24
|
+
parts = args.split(maxsplit=1)
|
|
25
|
+
if len(parts) < 2:
|
|
26
|
+
return "Usage: alias <name> <expansion>"
|
|
27
|
+
name, expansion = parts[0].strip(), parts[1].strip()
|
|
28
|
+
# Prevent self-referential aliases by simple guard.
|
|
29
|
+
if name.lower() in ("alias", "unalias"):
|
|
30
|
+
return "You cannot alias that command."
|
|
31
|
+
char.aliases[name] = expansion
|
|
32
|
+
return f"Alias set: {name} -> {expansion}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def do_unalias(char: Character, args: str = "") -> str:
|
|
36
|
+
"""Remove an alias by name."""
|
|
37
|
+
name = args.strip()
|
|
38
|
+
if not name:
|
|
39
|
+
return "Usage: unalias <name>"
|
|
40
|
+
if name in char.aliases:
|
|
41
|
+
del char.aliases[name]
|
|
42
|
+
return f"Removed alias: {name}"
|
|
43
|
+
return "No such alias."
|
|
44
|
+
|
mud/commands/build.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from mud.models.character import Character
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def cmd_redit(char: Character, args: str) -> str:
|
|
5
|
+
"""Edit the current room's fields."""
|
|
6
|
+
if not char.room:
|
|
7
|
+
return "You are nowhere."
|
|
8
|
+
parts = args.split(maxsplit=1)
|
|
9
|
+
if len(parts) != 2:
|
|
10
|
+
return "Usage: @redit name|desc <value>"
|
|
11
|
+
field, value = parts
|
|
12
|
+
if field == "name":
|
|
13
|
+
char.room.name = value
|
|
14
|
+
return f"Room name set to {value}"
|
|
15
|
+
if field in {"desc", "description"}:
|
|
16
|
+
char.room.description = value
|
|
17
|
+
return "Room description updated."
|
|
18
|
+
return "Invalid field."
|
mud/commands/combat.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from mud.models.character import Character
|
|
2
|
+
from mud.combat import attack_round
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def do_kill(char: Character, args: str) -> str:
|
|
6
|
+
if not args:
|
|
7
|
+
return "Kill whom?"
|
|
8
|
+
target_name = args.lower()
|
|
9
|
+
if not getattr(char, "room", None):
|
|
10
|
+
return "You are nowhere."
|
|
11
|
+
for victim in list(char.room.people):
|
|
12
|
+
if victim is char:
|
|
13
|
+
continue
|
|
14
|
+
if victim.name and target_name in victim.name.lower():
|
|
15
|
+
return attack_round(char, victim)
|
|
16
|
+
return "They aren't here."
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from mud.models.character import Character, character_registry
|
|
5
|
+
from mud.net.protocol import broadcast_room, broadcast_global, send_to_char
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def do_say(char: Character, args: str) -> str:
|
|
9
|
+
if not args:
|
|
10
|
+
return "Say what?"
|
|
11
|
+
message = f"{char.name} says, '{args}'"
|
|
12
|
+
if char.room:
|
|
13
|
+
char.room.broadcast(message, exclude=char)
|
|
14
|
+
broadcast_room(char.room, message, exclude=char)
|
|
15
|
+
return f"You say, '{args}'"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def do_tell(char: Character, args: str) -> str:
|
|
19
|
+
if "tell" in char.banned_channels:
|
|
20
|
+
return "You are banned from tell."
|
|
21
|
+
if not args:
|
|
22
|
+
return "Tell whom what?"
|
|
23
|
+
try:
|
|
24
|
+
target_name, message = args.split(None, 1)
|
|
25
|
+
except ValueError:
|
|
26
|
+
return "Tell whom what?"
|
|
27
|
+
target = next(
|
|
28
|
+
(
|
|
29
|
+
c
|
|
30
|
+
for c in character_registry
|
|
31
|
+
if c.name and c.name.lower() == target_name.lower()
|
|
32
|
+
),
|
|
33
|
+
None,
|
|
34
|
+
)
|
|
35
|
+
if not target:
|
|
36
|
+
return "They aren't here."
|
|
37
|
+
if "tell" in target.muted_channels:
|
|
38
|
+
return "They aren't listening."
|
|
39
|
+
text = f"{char.name} tells you, '{message}'"
|
|
40
|
+
writer = getattr(target, "connection", None)
|
|
41
|
+
if writer:
|
|
42
|
+
asyncio.create_task(send_to_char(target, text))
|
|
43
|
+
if hasattr(target, "messages"):
|
|
44
|
+
target.messages.append(text)
|
|
45
|
+
return f"You tell {target.name}, '{message}'"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def do_shout(char: Character, args: str) -> str:
|
|
49
|
+
if "shout" in char.banned_channels:
|
|
50
|
+
return "You are banned from shout."
|
|
51
|
+
if not args:
|
|
52
|
+
return "Shout what?"
|
|
53
|
+
message = f"{char.name} shouts, '{args}'"
|
|
54
|
+
broadcast_global(message, channel="shout", exclude=char)
|
|
55
|
+
return f"You shout, '{args}'"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from mud.models.character import Character
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def admin_only(func):
|
|
6
|
+
@wraps(func)
|
|
7
|
+
def wrapper(char: Character, *args, **kwargs):
|
|
8
|
+
if not getattr(char, "is_admin", False):
|
|
9
|
+
return "You do not have permission to use this command."
|
|
10
|
+
return func(char, *args, **kwargs)
|
|
11
|
+
return wrapper
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import shlex
|
|
4
|
+
from typing import Callable, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from mud.models.character import Character
|
|
7
|
+
from .movement import do_north, do_south, do_east, do_west, do_up, do_down, do_enter
|
|
8
|
+
from .inspection import do_look, do_scan, do_exits
|
|
9
|
+
from .inventory import do_get, do_drop, do_inventory, do_equipment
|
|
10
|
+
from .communication import do_say, do_tell, do_shout
|
|
11
|
+
from .combat import do_kill
|
|
12
|
+
from .admin_commands import (
|
|
13
|
+
cmd_who,
|
|
14
|
+
cmd_teleport,
|
|
15
|
+
cmd_spawn,
|
|
16
|
+
cmd_ban,
|
|
17
|
+
cmd_unban,
|
|
18
|
+
cmd_banlist,
|
|
19
|
+
)
|
|
20
|
+
from .shop import do_list, do_buy, do_sell
|
|
21
|
+
from .healer import do_heal
|
|
22
|
+
from .alias_cmds import do_alias, do_unalias
|
|
23
|
+
from .advancement import do_practice, do_train
|
|
24
|
+
from .notes import do_board, do_note
|
|
25
|
+
from .build import cmd_redit
|
|
26
|
+
from .socials import perform_social
|
|
27
|
+
from .help import do_help
|
|
28
|
+
from .imc import do_imc
|
|
29
|
+
from mud.wiznet import cmd_wiznet
|
|
30
|
+
from mud.logging.admin import log_admin_command
|
|
31
|
+
from mud.models.social import social_registry
|
|
32
|
+
from mud.models.constants import Position
|
|
33
|
+
|
|
34
|
+
CommandFunc = Callable[[Character, str], str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Command:
|
|
39
|
+
name: str
|
|
40
|
+
func: CommandFunc
|
|
41
|
+
aliases: tuple[str, ...] = ()
|
|
42
|
+
admin_only: bool = False
|
|
43
|
+
min_position: Position = Position.DEAD
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
COMMANDS: List[Command] = [
|
|
47
|
+
# Movement (require standing per ROM)
|
|
48
|
+
Command("north", do_north, aliases=("n",), min_position=Position.STANDING),
|
|
49
|
+
Command("east", do_east, aliases=("e",), min_position=Position.STANDING),
|
|
50
|
+
Command("south", do_south, aliases=("s",), min_position=Position.STANDING),
|
|
51
|
+
Command("west", do_west, aliases=("w",), min_position=Position.STANDING),
|
|
52
|
+
Command("up", do_up, aliases=("u",), min_position=Position.STANDING),
|
|
53
|
+
Command("down", do_down, aliases=("d",), min_position=Position.STANDING),
|
|
54
|
+
Command("enter", do_enter, min_position=Position.STANDING),
|
|
55
|
+
|
|
56
|
+
# Common actions
|
|
57
|
+
Command("look", do_look, aliases=("l",), min_position=Position.RESTING),
|
|
58
|
+
Command("exits", do_exits, aliases=("ex",), min_position=Position.RESTING),
|
|
59
|
+
Command("get", do_get, aliases=("g",), min_position=Position.RESTING),
|
|
60
|
+
Command("drop", do_drop, min_position=Position.RESTING),
|
|
61
|
+
Command("inventory", do_inventory, aliases=("inv",), min_position=Position.DEAD),
|
|
62
|
+
Command("equipment", do_equipment, aliases=("eq",), min_position=Position.DEAD),
|
|
63
|
+
|
|
64
|
+
# Communication
|
|
65
|
+
Command("say", do_say, min_position=Position.RESTING),
|
|
66
|
+
Command("tell", do_tell, min_position=Position.RESTING),
|
|
67
|
+
Command("shout", do_shout, min_position=Position.RESTING),
|
|
68
|
+
|
|
69
|
+
# Combat
|
|
70
|
+
Command("kill", do_kill, aliases=("attack",), min_position=Position.FIGHTING),
|
|
71
|
+
|
|
72
|
+
# Info
|
|
73
|
+
Command("scan", do_scan, min_position=Position.SLEEPING),
|
|
74
|
+
|
|
75
|
+
# Shops
|
|
76
|
+
Command("list", do_list, min_position=Position.RESTING),
|
|
77
|
+
Command("buy", do_buy, min_position=Position.RESTING),
|
|
78
|
+
Command("sell", do_sell, min_position=Position.RESTING),
|
|
79
|
+
Command("heal", do_heal, min_position=Position.RESTING),
|
|
80
|
+
|
|
81
|
+
# Advancement
|
|
82
|
+
Command("practice", do_practice, min_position=Position.SLEEPING),
|
|
83
|
+
Command("train", do_train, min_position=Position.RESTING),
|
|
84
|
+
|
|
85
|
+
# Boards/Notes/Help
|
|
86
|
+
Command("board", do_board, min_position=Position.SLEEPING),
|
|
87
|
+
Command("note", do_note, min_position=Position.DEAD),
|
|
88
|
+
Command("help", do_help, min_position=Position.DEAD),
|
|
89
|
+
|
|
90
|
+
# IMC and aliasing
|
|
91
|
+
Command("imc", do_imc, min_position=Position.DEAD),
|
|
92
|
+
Command("alias", do_alias, min_position=Position.DEAD),
|
|
93
|
+
Command("unalias", do_unalias, min_position=Position.DEAD),
|
|
94
|
+
|
|
95
|
+
# Admin (leave position as DEAD; admin-only gating applies separately)
|
|
96
|
+
Command("@who", cmd_who, admin_only=True),
|
|
97
|
+
Command("@teleport", cmd_teleport, admin_only=True),
|
|
98
|
+
Command("@spawn", cmd_spawn, admin_only=True),
|
|
99
|
+
Command("ban", cmd_ban, admin_only=True),
|
|
100
|
+
Command("unban", cmd_unban, admin_only=True),
|
|
101
|
+
Command("banlist", cmd_banlist, admin_only=True),
|
|
102
|
+
Command("@redit", cmd_redit, admin_only=True),
|
|
103
|
+
Command("wiznet", cmd_wiznet, admin_only=True),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
COMMAND_INDEX: Dict[str, Command] = {}
|
|
108
|
+
for cmd in COMMANDS:
|
|
109
|
+
COMMAND_INDEX[cmd.name] = cmd
|
|
110
|
+
for alias in cmd.aliases:
|
|
111
|
+
COMMAND_INDEX[alias] = cmd
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def resolve_command(name: str) -> Optional[Command]:
|
|
115
|
+
name = name.lower()
|
|
116
|
+
if name in COMMAND_INDEX:
|
|
117
|
+
return COMMAND_INDEX[name]
|
|
118
|
+
# ROM str_prefix behavior: choose the first command in table order
|
|
119
|
+
# whose name starts with the provided prefix. If none match, return None.
|
|
120
|
+
matches = [cmd for cmd in COMMANDS if cmd.name.startswith(name)]
|
|
121
|
+
return matches[0] if matches else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _expand_aliases(char: Character, input_str: str, *, max_depth: int = 5) -> str:
|
|
125
|
+
"""Expand the first token using per-character aliases, up to max_depth."""
|
|
126
|
+
s = input_str
|
|
127
|
+
for _ in range(max_depth):
|
|
128
|
+
try:
|
|
129
|
+
parts = shlex.split(s)
|
|
130
|
+
except ValueError:
|
|
131
|
+
return s
|
|
132
|
+
if not parts:
|
|
133
|
+
return s
|
|
134
|
+
head, tail = parts[0], parts[1:]
|
|
135
|
+
expansion = char.aliases.get(head)
|
|
136
|
+
if not expansion:
|
|
137
|
+
return s
|
|
138
|
+
s = (expansion + (" " + " ".join(tail) if tail else "")).strip()
|
|
139
|
+
return s
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def process_command(char: Character, input_str: str) -> str:
|
|
143
|
+
if not input_str.strip():
|
|
144
|
+
return "What?"
|
|
145
|
+
expanded = _expand_aliases(char, input_str)
|
|
146
|
+
try:
|
|
147
|
+
parts = shlex.split(expanded)
|
|
148
|
+
except ValueError:
|
|
149
|
+
return "Huh?"
|
|
150
|
+
if not parts:
|
|
151
|
+
return "What?"
|
|
152
|
+
cmd_name, *args = parts
|
|
153
|
+
command = resolve_command(cmd_name)
|
|
154
|
+
if not command:
|
|
155
|
+
social = social_registry.get(cmd_name.lower())
|
|
156
|
+
if social:
|
|
157
|
+
return perform_social(char, cmd_name, " ".join(args))
|
|
158
|
+
return "Huh?"
|
|
159
|
+
if command.admin_only and not getattr(char, "is_admin", False):
|
|
160
|
+
return "You do not have permission to use this command."
|
|
161
|
+
# Position gating (ROM-compatible messages)
|
|
162
|
+
if char.position < command.min_position:
|
|
163
|
+
pos = char.position
|
|
164
|
+
if pos == Position.DEAD:
|
|
165
|
+
return "Lie still; you are DEAD."
|
|
166
|
+
if pos in (Position.MORTAL, Position.INCAP):
|
|
167
|
+
return "You are hurt far too bad for that."
|
|
168
|
+
if pos == Position.STUNNED:
|
|
169
|
+
return "You are too stunned to do that."
|
|
170
|
+
if pos == Position.SLEEPING:
|
|
171
|
+
return "In your dreams, or what?"
|
|
172
|
+
if pos == Position.RESTING:
|
|
173
|
+
return "Nah... You feel too relaxed..."
|
|
174
|
+
if pos == Position.SITTING:
|
|
175
|
+
return "Better stand up first."
|
|
176
|
+
if pos == Position.FIGHTING:
|
|
177
|
+
return "No way! You are still fighting!"
|
|
178
|
+
# Fallback (should not happen)
|
|
179
|
+
return "You can't do that right now."
|
|
180
|
+
arg_str = " ".join(args)
|
|
181
|
+
# Log admin commands (accepted) to admin log for auditability.
|
|
182
|
+
if command.admin_only and getattr(char, "is_admin", False):
|
|
183
|
+
try:
|
|
184
|
+
log_admin_command(getattr(char, "name", "?"), command.name, arg_str)
|
|
185
|
+
except Exception:
|
|
186
|
+
# Logging must never break command execution.
|
|
187
|
+
pass
|
|
188
|
+
return command.func(char, arg_str)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def run_test_session() -> list[str]:
|
|
192
|
+
from mud.world import initialize_world, create_test_character
|
|
193
|
+
from mud.spawning.obj_spawner import spawn_object
|
|
194
|
+
|
|
195
|
+
initialize_world('area/area.lst')
|
|
196
|
+
char = create_test_character('Tester', 3001)
|
|
197
|
+
# Ensure sufficient movement points for the scripted walk
|
|
198
|
+
char.move = char.max_move = 100
|
|
199
|
+
sword = spawn_object(3022)
|
|
200
|
+
if sword:
|
|
201
|
+
char.room.add_object(sword)
|
|
202
|
+
commands = ["look", "get sword", "north", "say hello"]
|
|
203
|
+
outputs = []
|
|
204
|
+
for line in commands:
|
|
205
|
+
outputs.append(process_command(char, line))
|
|
206
|
+
return outputs
|
mud/commands/healer.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mud.models.character import Character
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _find_healer(char: Character) -> Optional[object]:
|
|
9
|
+
"""Find a healer NPC in the room.
|
|
10
|
+
|
|
11
|
+
Heuristic: a mob whose prototype has spec_fun == 'spec_healer',
|
|
12
|
+
or any room occupant with an attribute `is_healer` truthy.
|
|
13
|
+
"""
|
|
14
|
+
for mob in getattr(char.room, "people", []):
|
|
15
|
+
if getattr(mob, "is_healer", False):
|
|
16
|
+
return mob
|
|
17
|
+
proto = getattr(mob, "prototype", None)
|
|
18
|
+
if proto:
|
|
19
|
+
spec = getattr(proto, "spec_fun", "") or ""
|
|
20
|
+
if spec.lower() == "spec_healer":
|
|
21
|
+
return mob
|
|
22
|
+
return mob
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
PRICE_GOLD = {
|
|
27
|
+
"light": 10,
|
|
28
|
+
"serious": 15,
|
|
29
|
+
"critical": 25,
|
|
30
|
+
"heal": 50,
|
|
31
|
+
"blindness": 20,
|
|
32
|
+
"disease": 15,
|
|
33
|
+
"poison": 25,
|
|
34
|
+
"uncurse": 50,
|
|
35
|
+
"refresh": 5,
|
|
36
|
+
"mana": 10,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def do_heal(char: Character, args: str = "") -> str:
|
|
41
|
+
healer = _find_healer(char)
|
|
42
|
+
if not healer:
|
|
43
|
+
return "You can't do that here."
|
|
44
|
+
|
|
45
|
+
arg = (args or "").strip().lower()
|
|
46
|
+
if not arg:
|
|
47
|
+
# Minimal price list in ROM spirit
|
|
48
|
+
items = "; ".join(f"{k} {v} gold" for k, v in PRICE_GOLD.items())
|
|
49
|
+
return f"Healer offers: {items}"
|
|
50
|
+
|
|
51
|
+
# Normalize common aliases
|
|
52
|
+
if arg.startswith("critic"):
|
|
53
|
+
arg = "critical"
|
|
54
|
+
if arg.startswith("uncurse") or arg == "curse":
|
|
55
|
+
arg = "uncurse"
|
|
56
|
+
|
|
57
|
+
if arg not in PRICE_GOLD:
|
|
58
|
+
return "Type heal for a list of spells."
|
|
59
|
+
|
|
60
|
+
cost = PRICE_GOLD[arg]
|
|
61
|
+
if char.gold < cost:
|
|
62
|
+
return "You do not have enough gold for my services."
|
|
63
|
+
|
|
64
|
+
char.gold -= cost
|
|
65
|
+
|
|
66
|
+
# Apply simple effects sufficient for parity tests
|
|
67
|
+
if arg == "refresh":
|
|
68
|
+
char.move = min(char.max_move, max(char.move, char.max_move))
|
|
69
|
+
return "You feel refreshed."
|
|
70
|
+
if arg == "heal":
|
|
71
|
+
char.hit = min(char.max_hit, max(char.hit, char.max_hit))
|
|
72
|
+
return "Your wounds mend."
|
|
73
|
+
if arg == "mana":
|
|
74
|
+
char.mana = min(char.max_mana, char.mana + 10)
|
|
75
|
+
return "A warm glow passes through you."
|
|
76
|
+
if arg in ("light", "serious", "critical"):
|
|
77
|
+
inc = {"light": 10, "serious": 20, "critical": 30}[arg]
|
|
78
|
+
char.hit = min(char.max_hit, char.hit + inc)
|
|
79
|
+
return "You feel better."
|
|
80
|
+
# For status cures, just acknowledge.
|
|
81
|
+
return "You feel cleansed."
|
mud/commands/help.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from mud.models.character import Character
|
|
4
|
+
from mud.models.help import help_registry
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def do_help(ch: Character, args: str) -> str:
|
|
8
|
+
topic = args.strip().lower()
|
|
9
|
+
if not topic:
|
|
10
|
+
return "Help what?"
|
|
11
|
+
entry = help_registry.get(topic)
|
|
12
|
+
if not entry:
|
|
13
|
+
return "No help on that word."
|
|
14
|
+
return entry.text
|
mud/commands/imc.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from mud.imc import imc_enabled
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def do_imc(char, args: str) -> str:
|
|
7
|
+
"""IMC command stub.
|
|
8
|
+
|
|
9
|
+
- Disabled (default): returns a gated message.
|
|
10
|
+
- Enabled: returns basic help/usage; no sockets opened here.
|
|
11
|
+
"""
|
|
12
|
+
if not imc_enabled():
|
|
13
|
+
return "IMC is disabled. Set IMC_ENABLED=true to enable."
|
|
14
|
+
|
|
15
|
+
if not args or args.strip().lower() in {"help", "?"}:
|
|
16
|
+
return "IMC is enabled (stub). Usage: imc send <channel> <message>"
|
|
17
|
+
|
|
18
|
+
return "IMC stub: command not implemented."
|
|
19
|
+
|