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,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from mud.models.character import Character
|
|
4
|
+
from mud.world.look import look, dir_names
|
|
5
|
+
from mud.models.constants import Direction
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def do_scan(char: Character, args: str = "") -> str:
|
|
9
|
+
"""ROM-like scan output with distances and optional direction.
|
|
10
|
+
|
|
11
|
+
- No arg: list current room (depth 0) and adjacent rooms (depth 1) in N,E,S,W,Up,Down order.
|
|
12
|
+
- With direction: follow exits up to depth 3 and list visible characters per room.
|
|
13
|
+
"""
|
|
14
|
+
if not char.room:
|
|
15
|
+
return "You see nothing."
|
|
16
|
+
|
|
17
|
+
order = [
|
|
18
|
+
Direction.NORTH,
|
|
19
|
+
Direction.EAST,
|
|
20
|
+
Direction.SOUTH,
|
|
21
|
+
Direction.WEST,
|
|
22
|
+
Direction.UP,
|
|
23
|
+
Direction.DOWN,
|
|
24
|
+
]
|
|
25
|
+
dir_name = {
|
|
26
|
+
Direction.NORTH: "north",
|
|
27
|
+
Direction.EAST: "east",
|
|
28
|
+
Direction.SOUTH: "south",
|
|
29
|
+
Direction.WEST: "west",
|
|
30
|
+
Direction.UP: "up",
|
|
31
|
+
Direction.DOWN: "down",
|
|
32
|
+
}
|
|
33
|
+
distance = [
|
|
34
|
+
"right here.",
|
|
35
|
+
"nearby to the %s.",
|
|
36
|
+
"not far %s.",
|
|
37
|
+
"off in the distance %s.",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def list_room(room, depth: int, door: int) -> list[str]:
|
|
41
|
+
lines: list[str] = []
|
|
42
|
+
if not room:
|
|
43
|
+
return lines
|
|
44
|
+
for p in room.people:
|
|
45
|
+
if p is char:
|
|
46
|
+
continue
|
|
47
|
+
who = p.name or "someone"
|
|
48
|
+
if depth == 0:
|
|
49
|
+
lines.append(f"{who}, {distance[0]}")
|
|
50
|
+
else:
|
|
51
|
+
dn = dir_name[Direction(door)]
|
|
52
|
+
lines.append(f"{who}, {distance[depth] % dn}")
|
|
53
|
+
return lines
|
|
54
|
+
|
|
55
|
+
s = args.strip().lower()
|
|
56
|
+
if not s:
|
|
57
|
+
lines: list[str] = ["Looking around you see:"]
|
|
58
|
+
# current room
|
|
59
|
+
lines += list_room(char.room, 0, -1)
|
|
60
|
+
# each direction at depth 1
|
|
61
|
+
for d in order:
|
|
62
|
+
ex = char.room.exits[int(d)] if char.room.exits and int(d) < len(char.room.exits) else None
|
|
63
|
+
to_room = ex.to_room if ex else None
|
|
64
|
+
lines += list_room(to_room, 1, int(d))
|
|
65
|
+
if len(lines) == 1:
|
|
66
|
+
lines.append("No one is nearby.")
|
|
67
|
+
return "\n".join(lines)
|
|
68
|
+
|
|
69
|
+
# Directional scan up to depth 3
|
|
70
|
+
token_map = {
|
|
71
|
+
"n": Direction.NORTH,
|
|
72
|
+
"north": Direction.NORTH,
|
|
73
|
+
"e": Direction.EAST,
|
|
74
|
+
"east": Direction.EAST,
|
|
75
|
+
"s": Direction.SOUTH,
|
|
76
|
+
"south": Direction.SOUTH,
|
|
77
|
+
"w": Direction.WEST,
|
|
78
|
+
"west": Direction.WEST,
|
|
79
|
+
"u": Direction.UP,
|
|
80
|
+
"up": Direction.UP,
|
|
81
|
+
"d": Direction.DOWN,
|
|
82
|
+
"down": Direction.DOWN,
|
|
83
|
+
}
|
|
84
|
+
if s not in token_map:
|
|
85
|
+
return "Which way do you want to scan?"
|
|
86
|
+
d = token_map[s]
|
|
87
|
+
dir_str = dir_name[d]
|
|
88
|
+
lines = [f"Looking {dir_str} you see:"]
|
|
89
|
+
scan_room = char.room
|
|
90
|
+
for depth in (1, 2, 3):
|
|
91
|
+
ex = scan_room.exits[int(d)] if scan_room and scan_room.exits and int(d) < len(scan_room.exits) else None
|
|
92
|
+
scan_room = ex.to_room if ex else None
|
|
93
|
+
if not scan_room:
|
|
94
|
+
break
|
|
95
|
+
lines += list_room(scan_room, depth, int(d))
|
|
96
|
+
if len(lines) == 1:
|
|
97
|
+
lines.append("Nothing of note.")
|
|
98
|
+
return "\n".join(lines)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def do_look(char: Character, args: str = "") -> str:
|
|
102
|
+
return look(char)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def do_exits(char: Character, args: str = "") -> str:
|
|
106
|
+
"""List obvious exits from the current room (ROM-style)."""
|
|
107
|
+
room = char.room
|
|
108
|
+
if not room or not getattr(room, "exits", None):
|
|
109
|
+
return "Obvious exits: none."
|
|
110
|
+
dirs = [dir_names[type(list(dir_names.keys())[0]) (i)] for i, ex in enumerate(room.exits) if ex]
|
|
111
|
+
if not dirs:
|
|
112
|
+
return "Obvious exits: none."
|
|
113
|
+
return f"Obvious exits: {' '.join(dirs)}."
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from mud.models.character import Character
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def do_get(char: Character, args: str) -> str:
|
|
5
|
+
if not args:
|
|
6
|
+
return "Get what?"
|
|
7
|
+
name = args.lower()
|
|
8
|
+
for obj in list(char.room.contents):
|
|
9
|
+
obj_name = (obj.short_descr or obj.name or "").lower()
|
|
10
|
+
if name in obj_name:
|
|
11
|
+
char.room.contents.remove(obj)
|
|
12
|
+
char.add_object(obj)
|
|
13
|
+
return f"You pick up {obj.short_descr or obj.name}."
|
|
14
|
+
return "You don't see that here."
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def do_drop(char: Character, args: str) -> str:
|
|
18
|
+
if not args:
|
|
19
|
+
return "Drop what?"
|
|
20
|
+
name = args.lower()
|
|
21
|
+
for obj in list(char.inventory):
|
|
22
|
+
obj_name = (obj.short_descr or obj.name or "").lower()
|
|
23
|
+
if name in obj_name:
|
|
24
|
+
char.inventory.remove(obj)
|
|
25
|
+
char.room.add_object(obj)
|
|
26
|
+
return f"You drop {obj.short_descr or obj.name}."
|
|
27
|
+
return "You aren't carrying that."
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def do_inventory(char: Character, args: str = "") -> str:
|
|
31
|
+
if not char.inventory:
|
|
32
|
+
return "You are carrying nothing."
|
|
33
|
+
return "You are carrying: " + ", ".join(obj.short_descr or obj.name or "object" for obj in char.inventory)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def do_equipment(char: Character, args: str = "") -> str:
|
|
37
|
+
if not char.equipment:
|
|
38
|
+
return "You are wearing nothing."
|
|
39
|
+
parts = []
|
|
40
|
+
for slot, obj in char.equipment.items():
|
|
41
|
+
parts.append(f"{slot}: {obj.short_descr or obj.name or 'object'}")
|
|
42
|
+
return "You are using: " + ", ".join(parts)
|
mud/commands/movement.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from mud.world.movement import move_character
|
|
2
|
+
from mud.models.character import Character
|
|
3
|
+
from mud.models.constants import ItemType, EX_CLOSED
|
|
4
|
+
from mud.registry import room_registry
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def do_north(char: Character, args: str = "") -> str:
|
|
8
|
+
return move_character(char, "north")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def do_south(char: Character, args: str = "") -> str:
|
|
12
|
+
return move_character(char, "south")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def do_east(char: Character, args: str = "") -> str:
|
|
16
|
+
return move_character(char, "east")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def do_west(char: Character, args: str = "") -> str:
|
|
20
|
+
return move_character(char, "west")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def do_up(char: Character, args: str = "") -> str:
|
|
24
|
+
return move_character(char, "up")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def do_down(char: Character, args: str = "") -> str:
|
|
28
|
+
return move_character(char, "down")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def do_enter(char: Character, args: str = "") -> str:
|
|
32
|
+
target = (args or "").strip().lower()
|
|
33
|
+
if not target:
|
|
34
|
+
return "Enter what?"
|
|
35
|
+
|
|
36
|
+
# Find a portal object in the room matching target token
|
|
37
|
+
portal = None
|
|
38
|
+
for obj in getattr(char.room, "contents", []):
|
|
39
|
+
proto = getattr(obj, "prototype", None)
|
|
40
|
+
if not proto or getattr(proto, "item_type", 0) != int(ItemType.PORTAL):
|
|
41
|
+
continue
|
|
42
|
+
name = (getattr(proto, "short_descr", None) or getattr(proto, "name", "") or "").lower()
|
|
43
|
+
if target in name or target == "portal" or target in (getattr(obj, "short_descr", "") or "").lower():
|
|
44
|
+
portal = obj
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
if not portal:
|
|
48
|
+
return f"I see no {target} here."
|
|
49
|
+
|
|
50
|
+
flags = 0
|
|
51
|
+
proto = portal.prototype
|
|
52
|
+
values = getattr(proto, "value", [0, 0, 0, 0, 0])
|
|
53
|
+
if len(values) > 1 and isinstance(values[1], int):
|
|
54
|
+
flags = int(values[1])
|
|
55
|
+
|
|
56
|
+
if flags & EX_CLOSED:
|
|
57
|
+
return "The portal is closed."
|
|
58
|
+
|
|
59
|
+
dest_vnum = values[3] if len(values) > 3 else 0
|
|
60
|
+
dest = room_registry.get(int(dest_vnum))
|
|
61
|
+
if dest is None:
|
|
62
|
+
return "It doesn't seem to go anywhere."
|
|
63
|
+
|
|
64
|
+
# Move character
|
|
65
|
+
old_room = char.room
|
|
66
|
+
if char in old_room.people:
|
|
67
|
+
old_room.people.remove(char)
|
|
68
|
+
dest.people.append(char)
|
|
69
|
+
char.room = dest
|
|
70
|
+
char.wait = max(char.wait, 1)
|
|
71
|
+
return f"You enter the portal and arrive in {dest.name}."
|
mud/commands/notes.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from mud.models.character import Character
|
|
2
|
+
from mud.notes import board_registry, get_board, save_board
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def do_board(char: Character, args: str) -> str:
|
|
6
|
+
if not args:
|
|
7
|
+
if not board_registry:
|
|
8
|
+
return "No boards."
|
|
9
|
+
return "Boards: " + ", ".join(sorted(board_registry))
|
|
10
|
+
return "Huh?"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def do_note(char: Character, args: str) -> str:
|
|
14
|
+
if not args:
|
|
15
|
+
return "Note what?"
|
|
16
|
+
subcmd, *rest = args.split(None, 1)
|
|
17
|
+
rest_str = rest[0] if rest else ""
|
|
18
|
+
board = get_board("general")
|
|
19
|
+
if subcmd == "post":
|
|
20
|
+
if "|" not in rest_str:
|
|
21
|
+
return "Usage: note post <subject>|<text>"
|
|
22
|
+
subject, text = rest_str.split("|", 1)
|
|
23
|
+
board.post(char.name or "someone", subject.strip(), text.strip())
|
|
24
|
+
save_board(board)
|
|
25
|
+
return "Note posted."
|
|
26
|
+
elif subcmd == "list":
|
|
27
|
+
if not board.notes:
|
|
28
|
+
return "No notes."
|
|
29
|
+
lines = [
|
|
30
|
+
f"{i+1}: {note.subject} ({note.sender})"
|
|
31
|
+
for i, note in enumerate(board.notes)
|
|
32
|
+
]
|
|
33
|
+
return "\n".join(lines)
|
|
34
|
+
elif subcmd == "read":
|
|
35
|
+
try:
|
|
36
|
+
index = int(rest_str.strip()) - 1
|
|
37
|
+
except ValueError:
|
|
38
|
+
return "Read which note?"
|
|
39
|
+
if index < 0 or index >= len(board.notes):
|
|
40
|
+
return "No such note."
|
|
41
|
+
note = board.notes[index]
|
|
42
|
+
return f"{note.subject}\n{note.text}"
|
|
43
|
+
else:
|
|
44
|
+
return "Huh?"
|
mud/commands/shop.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Shop command handlers."""
|
|
2
|
+
|
|
3
|
+
from mud.registry import shop_registry
|
|
4
|
+
from mud.models.character import Character
|
|
5
|
+
from mud.models.object import Object
|
|
6
|
+
from mud.models.constants import ItemType
|
|
7
|
+
from mud.math.c_compat import c_div
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _find_shopkeeper(char: Character):
|
|
11
|
+
for mob in getattr(char.room, "people", []):
|
|
12
|
+
proto = getattr(mob, "prototype", None)
|
|
13
|
+
if proto and proto.vnum in shop_registry:
|
|
14
|
+
return mob
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_shop(keeper):
|
|
19
|
+
proto = getattr(keeper, "prototype", None)
|
|
20
|
+
if proto:
|
|
21
|
+
return shop_registry.get(proto.vnum)
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_cost(keeper, obj: Object, *, buy: bool) -> int:
|
|
26
|
+
"""Compute ROM-like shop price for an object.
|
|
27
|
+
|
|
28
|
+
Mirrors src/act_obj.c:get_cost:
|
|
29
|
+
- buy: base = obj.cost * profit_buy / 100
|
|
30
|
+
- sell: base = obj.cost * profit_sell / 100 if type accepted; otherwise 0
|
|
31
|
+
- inventory discount on sell when keeper already has same item:
|
|
32
|
+
- if existing copy has ITEM_INVENTORY → base /= 2
|
|
33
|
+
- else → base = base * 3 / 4
|
|
34
|
+
- wand/staff charge scaling: value[1]==0 → base/=4; else base = base * value[2] / value[1]
|
|
35
|
+
"""
|
|
36
|
+
proto = obj.prototype
|
|
37
|
+
shop = _get_shop(keeper)
|
|
38
|
+
if not shop:
|
|
39
|
+
return 0
|
|
40
|
+
cost = 0
|
|
41
|
+
if buy:
|
|
42
|
+
cost = c_div(getattr(proto, "cost", 0) * shop.profit_buy, 100)
|
|
43
|
+
else:
|
|
44
|
+
# ensure shop buys this type
|
|
45
|
+
item_type = getattr(proto, "item_type", 0)
|
|
46
|
+
if shop.buy_types and item_type not in shop.buy_types:
|
|
47
|
+
return 0
|
|
48
|
+
cost = c_div(getattr(proto, "cost", 0) * shop.profit_sell, 100)
|
|
49
|
+
# inventory discount if keeper already has same item
|
|
50
|
+
for other in getattr(keeper, "inventory", []) or []:
|
|
51
|
+
op = getattr(other, "prototype", None)
|
|
52
|
+
if not op:
|
|
53
|
+
continue
|
|
54
|
+
if op is proto or (
|
|
55
|
+
getattr(op, "vnum", None) == getattr(proto, "vnum", None)
|
|
56
|
+
and (getattr(op, "short_descr", None) or "") == (getattr(proto, "short_descr", None) or "")
|
|
57
|
+
):
|
|
58
|
+
# treat bit 1<<18 as ITEM_INVENTORY in this port
|
|
59
|
+
ITEM_INVENTORY = 1 << 18
|
|
60
|
+
if getattr(op, "extra_flags", 0) & ITEM_INVENTORY:
|
|
61
|
+
cost = c_div(cost, 2)
|
|
62
|
+
else:
|
|
63
|
+
cost = c_div(cost * 3, 4)
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
# Charge scaling for wand/staff
|
|
67
|
+
if getattr(proto, "item_type", 0) in (int(ItemType.WAND), int(ItemType.STAFF)):
|
|
68
|
+
vals = getattr(proto, "value", [0, 0, 0, 0, 0])
|
|
69
|
+
total = vals[1]
|
|
70
|
+
rem = vals[2]
|
|
71
|
+
if total == 0:
|
|
72
|
+
cost = c_div(cost, 4)
|
|
73
|
+
elif total > 0:
|
|
74
|
+
cost = c_div(cost * rem, total)
|
|
75
|
+
return max(0, int(cost))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def do_list(char: Character, args: str = "") -> str:
|
|
79
|
+
keeper = _find_shopkeeper(char)
|
|
80
|
+
if not keeper:
|
|
81
|
+
return "You can't do that here."
|
|
82
|
+
shop = _get_shop(keeper)
|
|
83
|
+
if not shop:
|
|
84
|
+
return "You can't do that here."
|
|
85
|
+
if not keeper.inventory:
|
|
86
|
+
return "The shop is out of stock."
|
|
87
|
+
items = []
|
|
88
|
+
for obj in keeper.inventory:
|
|
89
|
+
name = obj.short_descr or obj.name or "item"
|
|
90
|
+
price = _get_cost(keeper, obj, buy=True)
|
|
91
|
+
items.append(f"{name} {price} gold")
|
|
92
|
+
return "Items for sale: " + ", ".join(items)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def do_buy(char: Character, args: str) -> str:
|
|
96
|
+
if not args:
|
|
97
|
+
return "Buy what?"
|
|
98
|
+
keeper = _find_shopkeeper(char)
|
|
99
|
+
if not keeper:
|
|
100
|
+
return "You can't do that here."
|
|
101
|
+
shop = _get_shop(keeper)
|
|
102
|
+
if not shop:
|
|
103
|
+
return "You can't do that here."
|
|
104
|
+
name = args.lower()
|
|
105
|
+
for obj in list(keeper.inventory):
|
|
106
|
+
obj_name = (obj.short_descr or obj.name or "").lower()
|
|
107
|
+
if name in obj_name:
|
|
108
|
+
price = _get_cost(keeper, obj, buy=True)
|
|
109
|
+
if char.gold < price:
|
|
110
|
+
return "You can't afford that."
|
|
111
|
+
char.gold -= price
|
|
112
|
+
keeper.inventory.remove(obj)
|
|
113
|
+
char.add_object(obj)
|
|
114
|
+
return f"You buy {obj.short_descr or obj.name} for {price} gold."
|
|
115
|
+
return "The shopkeeper doesn't sell that."
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def do_sell(char: Character, args: str) -> str:
|
|
119
|
+
if not args:
|
|
120
|
+
return "Sell what?"
|
|
121
|
+
keeper = _find_shopkeeper(char)
|
|
122
|
+
if not keeper:
|
|
123
|
+
return "You can't do that here."
|
|
124
|
+
shop = _get_shop(keeper)
|
|
125
|
+
if not shop:
|
|
126
|
+
return "You can't do that here."
|
|
127
|
+
name = args.lower()
|
|
128
|
+
for obj in list(char.inventory):
|
|
129
|
+
obj_name = (obj.short_descr or obj.name or "").lower()
|
|
130
|
+
if name in obj_name:
|
|
131
|
+
price = _get_cost(keeper, obj, buy=False)
|
|
132
|
+
if price <= 0:
|
|
133
|
+
return "The shopkeeper doesn't buy that."
|
|
134
|
+
char.gold += price
|
|
135
|
+
char.inventory.remove(obj)
|
|
136
|
+
keeper.inventory.append(obj)
|
|
137
|
+
return f"You sell {obj.short_descr or obj.name} for {price} gold."
|
|
138
|
+
return "You don't have that."
|
mud/commands/socials.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from mud.models.character import Character
|
|
4
|
+
from mud.models.social import social_registry, expand_placeholders
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def perform_social(char: Character, name: str, arg: str) -> str:
|
|
8
|
+
social = social_registry.get(name.lower())
|
|
9
|
+
if social is None or char.room is None:
|
|
10
|
+
return "Huh?"
|
|
11
|
+
victim = None
|
|
12
|
+
if arg:
|
|
13
|
+
arg_lower = arg.lower()
|
|
14
|
+
for person in char.room.people:
|
|
15
|
+
if person is char:
|
|
16
|
+
continue
|
|
17
|
+
if getattr(person, "name", "").lower().startswith(arg_lower):
|
|
18
|
+
victim = person
|
|
19
|
+
break
|
|
20
|
+
if victim and victim is not char:
|
|
21
|
+
char.messages.append(expand_placeholders(social.char_found, char, victim))
|
|
22
|
+
char.room.broadcast(expand_placeholders(social.others_found, char, victim), exclude=char)
|
|
23
|
+
victim.messages.append(expand_placeholders(social.vict_found, char, victim))
|
|
24
|
+
elif arg and victim is char:
|
|
25
|
+
char.messages.append(expand_placeholders(social.char_auto, char))
|
|
26
|
+
char.room.broadcast(expand_placeholders(social.others_auto, char), exclude=char)
|
|
27
|
+
elif arg and not victim:
|
|
28
|
+
# ROM semantics: if an argument was provided but no victim is found,
|
|
29
|
+
# emit the "not found" message instead of the no-arg variant.
|
|
30
|
+
char.messages.append(expand_placeholders(social.not_found, char))
|
|
31
|
+
else:
|
|
32
|
+
char.messages.append(expand_placeholders(social.char_no_arg, char))
|
|
33
|
+
char.room.broadcast(expand_placeholders(social.others_no_arg, char), exclude=char)
|
|
34
|
+
return ""
|
mud/config.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
|
|
4
|
+
load_dotenv()
|
|
5
|
+
|
|
6
|
+
# Configuration for servers
|
|
7
|
+
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///mud.db")
|
|
8
|
+
PORT = int(os.getenv("PORT", 5000))
|
|
9
|
+
HOST = os.getenv("HOST", "0.0.0.0")
|
|
10
|
+
|
|
11
|
+
# Comma separated list of allowed CORS origins
|
|
12
|
+
CORS_ORIGINS = [origin.strip() for origin in os.getenv("CORS_ORIGINS", "*").split(",")]
|
|
13
|
+
|
|
14
|
+
# ----- ROM tick cadence (PULSE constants) -----
|
|
15
|
+
# ROM defines PULSE_PER_SECOND=4 and PULSE_TICK=60*PULSE_PER_SECOND (see src/merc.h)
|
|
16
|
+
# Keep these values here so engine code can reference parity timings.
|
|
17
|
+
PULSE_PER_SECOND: int = 4
|
|
18
|
+
|
|
19
|
+
def get_pulse_tick() -> int:
|
|
20
|
+
"""Return pulses per game tick hour (ROM PULSE_TICK).
|
|
21
|
+
|
|
22
|
+
Matches ROM's PULSE_TICK = 60 * PULSE_PER_SECOND.
|
|
23
|
+
"""
|
|
24
|
+
scale = max(1, int(os.getenv("TIME_SCALE", os.getenv("MUD_TIME_SCALE", "1")) or 1))
|
|
25
|
+
# Allow in-test overrides via module variable as well
|
|
26
|
+
try:
|
|
27
|
+
from mud import config as _cfg # local import to avoid cycles
|
|
28
|
+
scale = max(scale, int(getattr(_cfg, "TIME_SCALE", 1)))
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
base = 60 * PULSE_PER_SECOND
|
|
32
|
+
# Ensure at least 1 pulse per tick when scaled up
|
|
33
|
+
return max(1, base // scale)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_pulse_violence() -> int:
|
|
37
|
+
"""Return pulses per violence update (ROM PULSE_VIOLENCE).
|
|
38
|
+
|
|
39
|
+
ROM sets PULSE_VIOLENCE = 3 * PULSE_PER_SECOND.
|
|
40
|
+
Honor TIME_SCALE in the same way as ticks by dividing the base.
|
|
41
|
+
"""
|
|
42
|
+
scale = max(1, int(os.getenv("TIME_SCALE", os.getenv("MUD_TIME_SCALE", "1")) or 1))
|
|
43
|
+
try:
|
|
44
|
+
from mud import config as _cfg
|
|
45
|
+
scale = max(scale, int(getattr(_cfg, "TIME_SCALE", 1)))
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
base = 3 * PULSE_PER_SECOND
|
|
49
|
+
return max(1, base // scale)
|
|
50
|
+
|
|
51
|
+
# Feature flags
|
|
52
|
+
COMBAT_USE_THAC0: bool = False
|
|
53
|
+
|
|
54
|
+
# Optional test-only time scaling (1 = real ROM cadence)
|
|
55
|
+
TIME_SCALE: int = 1
|
|
56
|
+
|
|
57
|
+
# When True, schedule weather/reset strictly on point pulses (ROM-like).
|
|
58
|
+
# Default False to preserve existing test expectations.
|
|
59
|
+
GAME_LOOP_STRICT_POINT: bool = False
|
mud/db/__init__.py
ADDED
|
File without changes
|
mud/db/init.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from mud.db.models import Base
|
|
2
|
+
from mud.db.session import engine
|
|
3
|
+
from mud.db.seed import create_test_account
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def initialize_database():
|
|
8
|
+
"""Initialize the database with tables and seed data."""
|
|
9
|
+
# Create all tables
|
|
10
|
+
Base.metadata.create_all(engine)
|
|
11
|
+
|
|
12
|
+
# Create test admin account if it doesn't exist
|
|
13
|
+
create_test_account()
|
|
14
|
+
|
|
15
|
+
print(f"Database initialized at: {engine.url}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def database_exists():
|
|
19
|
+
"""Check if the database file exists."""
|
|
20
|
+
if str(engine.url).startswith('sqlite:///'):
|
|
21
|
+
db_path = str(engine.url).replace('sqlite:///', '')
|
|
22
|
+
return os.path.exists(db_path)
|
|
23
|
+
return True # For non-file databases
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
initialize_database()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""One-time migration script to populate the database from .are files."""
|
|
2
|
+
from mud.loaders import load_all_areas
|
|
3
|
+
from mud.world.linking import link_exits
|
|
4
|
+
from mud.registry import area_registry, room_registry, mob_registry, obj_registry
|
|
5
|
+
from mud.db.session import SessionLocal, engine
|
|
6
|
+
from mud.db import models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def migrate(area_list_path: str = "area/area.lst") -> None:
|
|
10
|
+
load_all_areas(area_list_path)
|
|
11
|
+
link_exits()
|
|
12
|
+
models.Base.metadata.create_all(engine)
|
|
13
|
+
session = SessionLocal()
|
|
14
|
+
|
|
15
|
+
area_map = {}
|
|
16
|
+
for area in area_registry.values():
|
|
17
|
+
db_area = models.Area(
|
|
18
|
+
vnum=area.vnum,
|
|
19
|
+
name=area.name,
|
|
20
|
+
min_vnum=area.min_vnum,
|
|
21
|
+
max_vnum=area.max_vnum,
|
|
22
|
+
)
|
|
23
|
+
session.add(db_area)
|
|
24
|
+
session.flush()
|
|
25
|
+
area_map[area] = db_area
|
|
26
|
+
|
|
27
|
+
room_map = {}
|
|
28
|
+
for room in room_registry.values():
|
|
29
|
+
db_room = models.Room(
|
|
30
|
+
vnum=room.vnum,
|
|
31
|
+
name=room.name,
|
|
32
|
+
description=room.description,
|
|
33
|
+
sector_type=room.sector_type,
|
|
34
|
+
room_flags=room.room_flags,
|
|
35
|
+
area=area_map.get(room.area),
|
|
36
|
+
)
|
|
37
|
+
session.add(db_room)
|
|
38
|
+
session.flush()
|
|
39
|
+
room_map[room] = db_room
|
|
40
|
+
|
|
41
|
+
for room in room_registry.values():
|
|
42
|
+
db_room = room_map[room]
|
|
43
|
+
for direction, exit_obj in enumerate(room.exits):
|
|
44
|
+
if exit_obj and exit_obj.vnum:
|
|
45
|
+
session.add(
|
|
46
|
+
models.Exit(
|
|
47
|
+
room=db_room,
|
|
48
|
+
direction=str(direction),
|
|
49
|
+
to_room_vnum=exit_obj.vnum,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
for mob in mob_registry.values():
|
|
54
|
+
session.add(
|
|
55
|
+
models.MobPrototype(
|
|
56
|
+
vnum=mob.vnum,
|
|
57
|
+
name=mob.player_name,
|
|
58
|
+
short_desc=mob.short_descr,
|
|
59
|
+
long_desc=mob.long_descr,
|
|
60
|
+
level=mob.level,
|
|
61
|
+
alignment=mob.alignment,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
for obj in obj_registry.values():
|
|
66
|
+
session.add(
|
|
67
|
+
models.ObjPrototype(
|
|
68
|
+
vnum=obj.vnum,
|
|
69
|
+
name=obj.name,
|
|
70
|
+
short_desc=obj.short_descr,
|
|
71
|
+
long_desc=obj.description,
|
|
72
|
+
item_type=obj.item_type,
|
|
73
|
+
flags=obj.extra_flags,
|
|
74
|
+
value0=obj.value[0] if len(obj.value) > 0 else None,
|
|
75
|
+
value1=obj.value[1] if len(obj.value) > 1 else None,
|
|
76
|
+
value2=obj.value[2] if len(obj.value) > 2 else None,
|
|
77
|
+
value3=obj.value[3] if len(obj.value) > 3 else None,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
session.commit()
|
|
82
|
+
session.close()
|
|
83
|
+
print("✅ Migration complete")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
migrate()
|