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
mud/skills/registry.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from random import Random
|
|
7
|
+
from typing import Callable, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from mud.models import Skill, SkillJson
|
|
10
|
+
from mud.utils import rng_mm
|
|
11
|
+
from mud.models.json_io import dataclass_from_dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SkillRegistry:
|
|
15
|
+
"""Load skill metadata from JSON and dispatch handlers."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, rng: Optional[Random] = None) -> None:
|
|
18
|
+
self.skills: Dict[str, Skill] = {}
|
|
19
|
+
self.handlers: Dict[str, Callable] = {}
|
|
20
|
+
self.rng = rng or Random()
|
|
21
|
+
|
|
22
|
+
def load(self, path: Path) -> None:
|
|
23
|
+
"""Load skill definitions from a JSON file."""
|
|
24
|
+
with path.open() as fp:
|
|
25
|
+
data = json.load(fp)
|
|
26
|
+
module = import_module("mud.skills.handlers")
|
|
27
|
+
for entry in data:
|
|
28
|
+
skill_json = dataclass_from_dict(SkillJson, entry)
|
|
29
|
+
skill = Skill.from_json(skill_json)
|
|
30
|
+
handler = getattr(module, skill.function)
|
|
31
|
+
self.skills[skill.name] = skill
|
|
32
|
+
self.handlers[skill.name] = handler
|
|
33
|
+
|
|
34
|
+
def get(self, name: str) -> Skill:
|
|
35
|
+
return self.skills[name]
|
|
36
|
+
|
|
37
|
+
def use(self, caster, name: str, target=None):
|
|
38
|
+
"""Execute a skill and handle resource costs and failure.
|
|
39
|
+
|
|
40
|
+
Parity: If the caster has a learned percentage for this skill in
|
|
41
|
+
`caster.skills[name]` (0..100), success is determined by a ROM-style
|
|
42
|
+
percent roll (number_percent) against that learned value. If no
|
|
43
|
+
learned value is present, fall back to `failure_rate` as before.
|
|
44
|
+
"""
|
|
45
|
+
skill = self.get(name)
|
|
46
|
+
if caster.mana < skill.mana_cost:
|
|
47
|
+
raise ValueError("not enough mana")
|
|
48
|
+
|
|
49
|
+
cooldowns = getattr(caster, "cooldowns", {})
|
|
50
|
+
if cooldowns.get(name, 0) > 0:
|
|
51
|
+
raise ValueError("skill on cooldown")
|
|
52
|
+
|
|
53
|
+
caster.mana -= skill.mana_cost
|
|
54
|
+
# ROM parity: prefer per-character learned% when available
|
|
55
|
+
learned = None
|
|
56
|
+
try:
|
|
57
|
+
learned = caster.skills.get(name) # 0..100
|
|
58
|
+
except Exception:
|
|
59
|
+
# Characters without a `skills` mapping should not error
|
|
60
|
+
learned = None
|
|
61
|
+
|
|
62
|
+
if learned is not None:
|
|
63
|
+
# Success when roll <= learned (ROM practice mechanics)
|
|
64
|
+
if rng_mm.number_percent() > int(learned):
|
|
65
|
+
cooldowns[name] = skill.cooldown
|
|
66
|
+
caster.cooldowns = cooldowns
|
|
67
|
+
return False
|
|
68
|
+
else:
|
|
69
|
+
# Fallback: use failure_rate gate (legacy behavior)
|
|
70
|
+
# Convert float failure_rate (0.0..1.0) to percentage threshold 0..100
|
|
71
|
+
failure_threshold = int(round(skill.failure_rate * 100))
|
|
72
|
+
if rng_mm.number_percent() <= failure_threshold:
|
|
73
|
+
cooldowns[name] = skill.cooldown
|
|
74
|
+
caster.cooldowns = cooldowns
|
|
75
|
+
return False
|
|
76
|
+
# Success path (roll > threshold): execute handler
|
|
77
|
+
|
|
78
|
+
result = self.handlers[name](caster, target)
|
|
79
|
+
cooldowns[name] = skill.cooldown
|
|
80
|
+
caster.cooldowns = cooldowns
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
def tick(self, character) -> None:
|
|
84
|
+
"""Reduce active cooldowns on a character by one tick."""
|
|
85
|
+
cooldowns = getattr(character, "cooldowns", {})
|
|
86
|
+
for key in list(cooldowns):
|
|
87
|
+
cooldowns[key] -= 1
|
|
88
|
+
if cooldowns[key] <= 0:
|
|
89
|
+
del cooldowns[key]
|
|
90
|
+
character.cooldowns = cooldowns
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
skill_registry = SkillRegistry()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_skills(path: Path) -> None:
|
|
97
|
+
skill_registry.load(path)
|
mud/spawning/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from mud.registry import mob_registry
|
|
5
|
+
from .templates import MobInstance
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def spawn_mob(vnum: int) -> Optional[MobInstance]:
|
|
9
|
+
proto = mob_registry.get(vnum)
|
|
10
|
+
if not proto:
|
|
11
|
+
return None
|
|
12
|
+
mob = MobInstance.from_prototype(proto)
|
|
13
|
+
return mob
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from mud.registry import obj_registry
|
|
5
|
+
from mud.models.object import Object
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def spawn_object(vnum: int) -> Optional[Object]:
|
|
9
|
+
proto = obj_registry.get(vnum)
|
|
10
|
+
if not proto:
|
|
11
|
+
return None
|
|
12
|
+
inst = Object(instance_id=None, prototype=proto)
|
|
13
|
+
# Copy prototype values for runtime mutation compatibility
|
|
14
|
+
try:
|
|
15
|
+
inst.value = list(getattr(proto, 'value', [0, 0, 0, 0, 0]))
|
|
16
|
+
except Exception:
|
|
17
|
+
inst.value = [0, 0, 0, 0, 0]
|
|
18
|
+
return inst
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from mud.models.area import Area
|
|
6
|
+
from mud.models.constants import ITEM_INVENTORY
|
|
7
|
+
from mud.registry import room_registry, area_registry
|
|
8
|
+
from .mob_spawner import spawn_mob
|
|
9
|
+
from .obj_spawner import spawn_object
|
|
10
|
+
from .templates import MobInstance
|
|
11
|
+
from mud.utils import rng_mm
|
|
12
|
+
|
|
13
|
+
RESET_TICKS = 3
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _compute_object_level(obj: object, mob: object) -> int:
|
|
17
|
+
"""Approximate ROM object level computation for G/E resets.
|
|
18
|
+
|
|
19
|
+
Mirrors src/db.c case 'G'/'E' for shopkeepers and equips (simplified):
|
|
20
|
+
- WAND: 10..20
|
|
21
|
+
- STAFF: 15..25
|
|
22
|
+
- ARMOR: 5..15
|
|
23
|
+
- WEAPON: 5..15
|
|
24
|
+
- TREASURE: 10..20
|
|
25
|
+
- Default: 0
|
|
26
|
+
For new-format objects, or unrecognized types, return 0.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
item_type = int(getattr(getattr(obj, 'prototype', None), 'item_type', 0))
|
|
30
|
+
except Exception:
|
|
31
|
+
item_type = 0
|
|
32
|
+
from mud.models.constants import ItemType
|
|
33
|
+
if item_type == int(ItemType.WAND):
|
|
34
|
+
return rng_mm.number_range(10, 20)
|
|
35
|
+
if item_type == int(ItemType.STAFF):
|
|
36
|
+
return rng_mm.number_range(15, 25)
|
|
37
|
+
if item_type == int(ItemType.ARMOR):
|
|
38
|
+
return rng_mm.number_range(5, 15)
|
|
39
|
+
if item_type == int(ItemType.WEAPON):
|
|
40
|
+
return rng_mm.number_range(5, 15)
|
|
41
|
+
if item_type == int(ItemType.TREASURE):
|
|
42
|
+
return rng_mm.number_range(10, 20)
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def apply_resets(area: Area) -> None:
|
|
47
|
+
"""Populate rooms based on simplified reset data."""
|
|
48
|
+
last_mob = None
|
|
49
|
+
last_obj: Optional[object] = None
|
|
50
|
+
# Track spawned objects per prototype vnum to support simple 'P' lookups
|
|
51
|
+
spawned_objects: Dict[int, List[object]] = {}
|
|
52
|
+
for reset in area.resets:
|
|
53
|
+
cmd = reset.command.upper()
|
|
54
|
+
if cmd == 'M':
|
|
55
|
+
mob_vnum = reset.arg2 or 0
|
|
56
|
+
room_vnum = reset.arg4 or 0
|
|
57
|
+
mob = spawn_mob(mob_vnum)
|
|
58
|
+
room = room_registry.get(room_vnum)
|
|
59
|
+
if mob and room:
|
|
60
|
+
room.add_mob(mob)
|
|
61
|
+
last_mob = mob
|
|
62
|
+
last_obj = None
|
|
63
|
+
else:
|
|
64
|
+
logging.warning('Invalid M reset %s -> %s', mob_vnum, room_vnum)
|
|
65
|
+
elif cmd == 'O':
|
|
66
|
+
obj_vnum = reset.arg2 or 0
|
|
67
|
+
room_vnum = reset.arg4 or 0
|
|
68
|
+
obj = spawn_object(obj_vnum)
|
|
69
|
+
room = room_registry.get(room_vnum)
|
|
70
|
+
if obj and room:
|
|
71
|
+
room.add_object(obj)
|
|
72
|
+
# Update last object instance and index by vnum
|
|
73
|
+
last_obj = obj
|
|
74
|
+
spawned_objects.setdefault(obj_vnum, []).append(obj)
|
|
75
|
+
else:
|
|
76
|
+
logging.warning('Invalid O reset %s -> %s', obj_vnum, room_vnum)
|
|
77
|
+
elif cmd == 'G':
|
|
78
|
+
obj_vnum = reset.arg2 or 0
|
|
79
|
+
limit = int(reset.arg3 or 1)
|
|
80
|
+
if not last_mob:
|
|
81
|
+
logging.warning('Invalid G reset %s (no LastMob)', obj_vnum)
|
|
82
|
+
continue
|
|
83
|
+
# Respect simple per-mob limit for this vnum
|
|
84
|
+
existing = [o for o in getattr(last_mob, 'inventory', [])
|
|
85
|
+
if getattr(getattr(o, 'prototype', None), 'vnum', None) == obj_vnum]
|
|
86
|
+
if len(existing) >= limit:
|
|
87
|
+
continue
|
|
88
|
+
obj = spawn_object(obj_vnum)
|
|
89
|
+
if obj:
|
|
90
|
+
obj.level = _compute_object_level(obj, last_mob)
|
|
91
|
+
# Shopkeepers receive inventory copies
|
|
92
|
+
# Detect shopkeeper by registry since pShop not wired on prototype
|
|
93
|
+
try:
|
|
94
|
+
from mud.registry import shop_registry
|
|
95
|
+
is_shopkeeper = getattr(getattr(last_mob, 'prototype', None), 'vnum', None) in shop_registry
|
|
96
|
+
except Exception:
|
|
97
|
+
is_shopkeeper = False
|
|
98
|
+
|
|
99
|
+
if is_shopkeeper:
|
|
100
|
+
if hasattr(obj.prototype, 'extra_flags'):
|
|
101
|
+
from mud.models.constants import ExtraFlag
|
|
102
|
+
if isinstance(obj.prototype.extra_flags, str):
|
|
103
|
+
# Legacy .are format uses flag letters - convert to proper flags
|
|
104
|
+
from mud.models.constants import convert_flags_from_letters
|
|
105
|
+
current_flags = convert_flags_from_letters(obj.prototype.extra_flags, ExtraFlag)
|
|
106
|
+
obj.prototype.extra_flags = current_flags | ITEM_INVENTORY
|
|
107
|
+
else:
|
|
108
|
+
obj.prototype.extra_flags |= ITEM_INVENTORY
|
|
109
|
+
last_mob.add_to_inventory(obj)
|
|
110
|
+
last_obj = obj
|
|
111
|
+
spawned_objects.setdefault(obj_vnum, []).append(obj)
|
|
112
|
+
else:
|
|
113
|
+
logging.warning('Invalid G reset %s', obj_vnum)
|
|
114
|
+
elif cmd == 'E':
|
|
115
|
+
obj_vnum = reset.arg2 or 0
|
|
116
|
+
limit = int(reset.arg3 or 1)
|
|
117
|
+
slot = reset.arg4 or 0
|
|
118
|
+
if not last_mob:
|
|
119
|
+
logging.warning('Invalid E reset %s (no LastMob)', obj_vnum)
|
|
120
|
+
continue
|
|
121
|
+
existing = [o for o in getattr(last_mob, 'inventory', [])
|
|
122
|
+
if getattr(getattr(o, 'prototype', None), 'vnum', None) == obj_vnum]
|
|
123
|
+
if len(existing) >= limit:
|
|
124
|
+
continue
|
|
125
|
+
obj = spawn_object(obj_vnum)
|
|
126
|
+
if obj:
|
|
127
|
+
obj.level = _compute_object_level(obj, last_mob)
|
|
128
|
+
try:
|
|
129
|
+
from mud.registry import shop_registry
|
|
130
|
+
is_shopkeeper = getattr(getattr(last_mob, 'prototype', None), 'vnum', None) in shop_registry
|
|
131
|
+
except Exception:
|
|
132
|
+
is_shopkeeper = False
|
|
133
|
+
if is_shopkeeper:
|
|
134
|
+
if hasattr(obj.prototype, 'extra_flags'):
|
|
135
|
+
from mud.models.constants import ExtraFlag
|
|
136
|
+
if isinstance(obj.prototype.extra_flags, str):
|
|
137
|
+
# Legacy .are format uses flag letters - convert to proper flags
|
|
138
|
+
from mud.models.constants import convert_flags_from_letters
|
|
139
|
+
current_flags = convert_flags_from_letters(obj.prototype.extra_flags, ExtraFlag)
|
|
140
|
+
obj.prototype.extra_flags = current_flags | ITEM_INVENTORY
|
|
141
|
+
else:
|
|
142
|
+
obj.prototype.extra_flags |= ITEM_INVENTORY
|
|
143
|
+
last_mob.equip(obj, slot)
|
|
144
|
+
last_obj = obj
|
|
145
|
+
spawned_objects.setdefault(obj_vnum, []).append(obj)
|
|
146
|
+
else:
|
|
147
|
+
logging.warning('Invalid E reset %s', obj_vnum)
|
|
148
|
+
elif cmd == 'P':
|
|
149
|
+
obj_vnum = reset.arg2 or 0
|
|
150
|
+
container_vnum = reset.arg4 or 0
|
|
151
|
+
count = max(1, int(reset.arg3 or 1)) # how many to place
|
|
152
|
+
if container_vnum <= 0:
|
|
153
|
+
logging.warning('Invalid P reset %s -> %s', obj_vnum, container_vnum)
|
|
154
|
+
continue
|
|
155
|
+
# Prefer the last created object instance if it matches the container vnum
|
|
156
|
+
container_obj: Optional[object] = None
|
|
157
|
+
if last_obj and getattr(getattr(last_obj, 'prototype', None), 'vnum', None) == container_vnum:
|
|
158
|
+
container_obj = last_obj
|
|
159
|
+
# Otherwise, fall back to the most recent spawned container by vnum
|
|
160
|
+
if not container_obj:
|
|
161
|
+
lst = spawned_objects.get(container_vnum) or []
|
|
162
|
+
container_obj = lst[-1] if lst else None
|
|
163
|
+
if not container_obj:
|
|
164
|
+
logging.warning('Invalid P reset %s -> %s (no container instance)', obj_vnum, container_vnum)
|
|
165
|
+
continue
|
|
166
|
+
# Determine existing count inside
|
|
167
|
+
existing = [o for o in getattr(container_obj, 'contained_items', [])
|
|
168
|
+
if getattr(getattr(o, 'prototype', None), 'vnum', None) == obj_vnum]
|
|
169
|
+
to_make = max(0, count - len(existing))
|
|
170
|
+
for _ in range(to_make):
|
|
171
|
+
obj = spawn_object(obj_vnum)
|
|
172
|
+
if not obj:
|
|
173
|
+
break
|
|
174
|
+
getattr(container_obj, 'contained_items').append(obj)
|
|
175
|
+
spawned_objects.setdefault(obj_vnum, []).append(obj)
|
|
176
|
+
# After population, set last_obj to the container (mirrors ROM behavior)
|
|
177
|
+
# Lock-state fix: reset container instance's value[1] to prototype's value[1]
|
|
178
|
+
try:
|
|
179
|
+
container_obj.value[1] = container_obj.prototype.value[1]
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
last_obj = container_obj
|
|
183
|
+
elif cmd == 'R':
|
|
184
|
+
room_vnum = reset.arg1 or 0
|
|
185
|
+
max_dirs = int(reset.arg2 or 0)
|
|
186
|
+
room = room_registry.get(room_vnum)
|
|
187
|
+
if not room or not room.exits:
|
|
188
|
+
logging.warning("Invalid R reset %s", room_vnum)
|
|
189
|
+
continue
|
|
190
|
+
n = min(max_dirs, len(room.exits))
|
|
191
|
+
# Fisher–Yates-like partial shuffle matching ROM loop
|
|
192
|
+
for d0 in range(0, max(0, n - 1)):
|
|
193
|
+
d1 = rng_mm.number_range(d0, n - 1)
|
|
194
|
+
room.exits[d0], room.exits[d1] = room.exits[d1], room.exits[d0]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def reset_area(area: Area) -> None:
|
|
198
|
+
"""Clear existing spawns and reapply resets for an area."""
|
|
199
|
+
for room in room_registry.values():
|
|
200
|
+
if room.area is area:
|
|
201
|
+
room.contents.clear()
|
|
202
|
+
room.people = [p for p in room.people if not isinstance(p, MobInstance)]
|
|
203
|
+
apply_resets(area)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def reset_tick() -> None:
|
|
207
|
+
"""Advance area ages and trigger resets when empty."""
|
|
208
|
+
for area in area_registry.values():
|
|
209
|
+
area.nplayer = sum(
|
|
210
|
+
1
|
|
211
|
+
for room in room_registry.values()
|
|
212
|
+
if room.area is area
|
|
213
|
+
for p in room.people
|
|
214
|
+
if not isinstance(p, MobInstance)
|
|
215
|
+
)
|
|
216
|
+
if area.nplayer > 0:
|
|
217
|
+
area.age = 0
|
|
218
|
+
continue
|
|
219
|
+
area.age += 1
|
|
220
|
+
if area.age >= RESET_TICKS:
|
|
221
|
+
reset_area(area)
|
|
222
|
+
area.age = 0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from mud.models.object import Object
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from mud.models.mob import MobIndex
|
|
11
|
+
from mud.models.obj import ObjIndex
|
|
12
|
+
from mud.models.object import Object
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ObjectInstance:
|
|
17
|
+
"""Runtime instance of an object."""
|
|
18
|
+
name: Optional[str]
|
|
19
|
+
item_type: int
|
|
20
|
+
prototype: ObjIndex
|
|
21
|
+
short_descr: Optional[str] = None
|
|
22
|
+
location: Optional['Room'] = None
|
|
23
|
+
contained_items: List['ObjectInstance'] = field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
def move_to_room(self, room: 'Room') -> None:
|
|
26
|
+
if self.location and hasattr(self.location, 'contents'):
|
|
27
|
+
if self in self.location.contents:
|
|
28
|
+
self.location.contents.remove(self)
|
|
29
|
+
room.contents.append(self)
|
|
30
|
+
self.location = room
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class MobInstance:
|
|
35
|
+
"""Runtime instance of a mob (NPC)."""
|
|
36
|
+
name: Optional[str]
|
|
37
|
+
level: int
|
|
38
|
+
current_hp: int
|
|
39
|
+
prototype: MobIndex
|
|
40
|
+
inventory: List[Object] = field(default_factory=list)
|
|
41
|
+
room: Optional['Room'] = None
|
|
42
|
+
# Minimal encumbrance fields to interoperate with move_character
|
|
43
|
+
carry_weight: int = 0
|
|
44
|
+
carry_number: int = 0
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_prototype(cls, proto: MobIndex) -> 'MobInstance':
|
|
48
|
+
return cls(name=proto.short_descr or proto.player_name,
|
|
49
|
+
level=proto.level,
|
|
50
|
+
current_hp=proto.hit[1],
|
|
51
|
+
prototype=proto)
|
|
52
|
+
|
|
53
|
+
def move_to_room(self, room: 'Room') -> None:
|
|
54
|
+
if self.room and self in self.room.people:
|
|
55
|
+
self.room.people.remove(self)
|
|
56
|
+
room.people.append(self)
|
|
57
|
+
self.room = room
|
|
58
|
+
|
|
59
|
+
def add_to_inventory(self, obj: Object) -> None:
|
|
60
|
+
self.inventory.append(obj)
|
|
61
|
+
|
|
62
|
+
def equip(self, obj: Object, slot: int) -> None: # stub
|
|
63
|
+
self.add_to_inventory(obj)
|
mud/spec_funs.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Callable
|
|
3
|
+
from mud.utils import rng_mm
|
|
4
|
+
|
|
5
|
+
spec_fun_registry: dict[str, Callable[..., Any]] = {}
|
|
6
|
+
|
|
7
|
+
def register_spec_fun(name: str, func: Callable[..., Any]) -> None:
|
|
8
|
+
"""Register *func* under *name*, storing key in lowercase."""
|
|
9
|
+
spec_fun_registry[name.lower()] = func
|
|
10
|
+
|
|
11
|
+
def get_spec_fun(name: str) -> Callable[..., Any] | None:
|
|
12
|
+
"""Return a spec_fun for *name* using case-insensitive lookup."""
|
|
13
|
+
return spec_fun_registry.get(name.lower())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_npc_specs() -> None:
|
|
17
|
+
"""Invoke registered spec_funs for NPCs in all rooms.
|
|
18
|
+
|
|
19
|
+
For each NPC (MobInstance) present in any room, if its prototype has a
|
|
20
|
+
non-empty ``spec_fun`` name and a function is registered under that name,
|
|
21
|
+
call it with the mob instance.
|
|
22
|
+
"""
|
|
23
|
+
from mud.registry import room_registry
|
|
24
|
+
|
|
25
|
+
for room in list(room_registry.values()):
|
|
26
|
+
for entity in list(getattr(room, "people", [])):
|
|
27
|
+
proto = getattr(entity, "prototype", None)
|
|
28
|
+
name = getattr(proto, "spec_fun", None)
|
|
29
|
+
if not name:
|
|
30
|
+
continue
|
|
31
|
+
func = get_spec_fun(name)
|
|
32
|
+
if func is None:
|
|
33
|
+
continue
|
|
34
|
+
try:
|
|
35
|
+
func(entity)
|
|
36
|
+
except Exception:
|
|
37
|
+
# Spec fun failures must not break the tick loop
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# --- Minimal ROM-like spec functions (rng_mm parity) ---
|
|
42
|
+
|
|
43
|
+
def spec_cast_adept(mob: Any) -> bool:
|
|
44
|
+
"""Simplified Adept spec that uses ROM RNG.
|
|
45
|
+
|
|
46
|
+
ROM's `spec_cast_adept` periodically casts helpful spells on players.
|
|
47
|
+
For parity of RNG usage, we roll `number_percent()` and return True when
|
|
48
|
+
the roll is small. This function exists primarily to validate that the
|
|
49
|
+
Mitchell–Moore RNG wiring matches ROM semantics.
|
|
50
|
+
"""
|
|
51
|
+
roll = rng_mm.number_percent()
|
|
52
|
+
# Return True on low roll to signal an action occurred (simplified).
|
|
53
|
+
return roll <= 25
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Convenience registration name matching ROM conventions
|
|
57
|
+
register_spec_fun("spec_cast_adept", spec_cast_adept)
|
mud/time.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Sunlight(IntEnum):
|
|
9
|
+
DARK = 0
|
|
10
|
+
RISE = 1
|
|
11
|
+
LIGHT = 2
|
|
12
|
+
SET = 3
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class TimeInfo:
|
|
17
|
+
hour: int = 0
|
|
18
|
+
day: int = 0
|
|
19
|
+
month: int = 0
|
|
20
|
+
year: int = 0
|
|
21
|
+
sunlight: Sunlight = Sunlight.DARK
|
|
22
|
+
|
|
23
|
+
def advance_hour(self) -> List[str]:
|
|
24
|
+
"""Advance time by one hour and return broadcast messages."""
|
|
25
|
+
messages: List[str] = []
|
|
26
|
+
self.hour += 1
|
|
27
|
+
if self.hour >= 24:
|
|
28
|
+
self.hour = 0
|
|
29
|
+
self.day += 1
|
|
30
|
+
if self.day >= 35:
|
|
31
|
+
self.day = 0
|
|
32
|
+
self.month += 1
|
|
33
|
+
if self.month >= 17:
|
|
34
|
+
self.month = 0
|
|
35
|
+
self.year += 1
|
|
36
|
+
if self.hour == 5:
|
|
37
|
+
self.sunlight = Sunlight.LIGHT
|
|
38
|
+
messages.append("The sun rises in the east.")
|
|
39
|
+
elif self.hour == 19:
|
|
40
|
+
self.sunlight = Sunlight.SET
|
|
41
|
+
messages.append("The sun slowly disappears in the west.")
|
|
42
|
+
elif self.hour == 20:
|
|
43
|
+
self.sunlight = Sunlight.DARK
|
|
44
|
+
messages.append("The night has begun.")
|
|
45
|
+
return messages
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
time_info = TimeInfo()
|
mud/utils/rng_mm.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Mitchell–Moore RNG for ROM parity.
|
|
2
|
+
|
|
3
|
+
Implements ROM's RNG surface with C-style gating:
|
|
4
|
+
- number_mm(): Mitchell–Moore generator (OLD_RAND branch in ROM) — returns a
|
|
5
|
+
non‑negative int with lower bits used by callers.
|
|
6
|
+
- number_percent(): returns 1..100 inclusive using bitmask + while-gate.
|
|
7
|
+
- number_range(a,b): ROM power-of-two mask with while-gate to avoid bias.
|
|
8
|
+
- number_bits(width): lower ``width`` bits of ``number_mm()``.
|
|
9
|
+
- dice(n,size): sum of ``number_range(1,size)`` repeated ``n`` times.
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
- C src/db.c:number_mm/number_percent/number_range/number_bits/dice
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
import time
|
|
18
|
+
import os
|
|
19
|
+
from typing import List
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
MASK_30 = (1 << 30) - 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class _MMState:
|
|
27
|
+
state: List[int]
|
|
28
|
+
i1: int
|
|
29
|
+
i2: int
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_mm: _MMState | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _init_state(seed: int) -> _MMState:
|
|
36
|
+
# Mirrors ROM init_mm() when OLD_RAND is defined
|
|
37
|
+
state = [0] * 55
|
|
38
|
+
i1 = 55 - 55 # 0
|
|
39
|
+
i2 = 55 - 24 # 31
|
|
40
|
+
state[0] = seed & MASK_30
|
|
41
|
+
state[1] = 1
|
|
42
|
+
for i in range(2, 55):
|
|
43
|
+
state[i] = (state[i - 1] + state[i - 2]) & MASK_30
|
|
44
|
+
return _MMState(state=state, i1=i1, i2=i2)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _ensure_mm() -> _MMState:
|
|
48
|
+
global _mm
|
|
49
|
+
if _mm is None:
|
|
50
|
+
# Default seed: time ^ pid like ROM's srandom(time ^ getpid) branch
|
|
51
|
+
seed = int(time.time()) ^ os.getpid()
|
|
52
|
+
_mm = _init_state(seed)
|
|
53
|
+
return _mm
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def seed_mm(seed: int) -> None:
|
|
57
|
+
"""Seed the Mitchell–Moore generator deterministically."""
|
|
58
|
+
global _mm
|
|
59
|
+
_mm = _init_state(int(seed))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def number_mm() -> int:
|
|
63
|
+
"""ROM Mitchell–Moore output with 6 LSBs discarded (>> 6 in callers).
|
|
64
|
+
|
|
65
|
+
We return the full value; public helpers mask/shift per ROM usage.
|
|
66
|
+
"""
|
|
67
|
+
mm = _ensure_mm()
|
|
68
|
+
iRand = (mm.state[mm.i1] + mm.state[mm.i2]) & MASK_30
|
|
69
|
+
mm.state[mm.i1] = iRand
|
|
70
|
+
mm.i1 += 1
|
|
71
|
+
if mm.i1 == 55:
|
|
72
|
+
mm.i1 = 0
|
|
73
|
+
mm.i2 += 1
|
|
74
|
+
if mm.i2 == 55:
|
|
75
|
+
mm.i2 = 0
|
|
76
|
+
# Return with 6 LSBs discarded to mirror ROM behavior.
|
|
77
|
+
return iRand >> 6
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def number_percent() -> int:
|
|
81
|
+
"""Return 1..100 inclusive using ROM's 7-bit mask + while-gate."""
|
|
82
|
+
# while ((percent = number_mm() & (128 - 1)) > 99);
|
|
83
|
+
# return 1 + percent;
|
|
84
|
+
percent = number_mm() & (128 - 1)
|
|
85
|
+
while percent > 99:
|
|
86
|
+
percent = number_mm() & (128 - 1)
|
|
87
|
+
return 1 + percent
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def number_range(a: int, b: int) -> int:
|
|
91
|
+
"""Return integer in [a, b] inclusive using power-of-two mask + gate."""
|
|
92
|
+
if a == 0 and b == 0:
|
|
93
|
+
return 0
|
|
94
|
+
if a > b:
|
|
95
|
+
a, b = b, a
|
|
96
|
+
to = b - a + 1
|
|
97
|
+
if to <= 1:
|
|
98
|
+
return a
|
|
99
|
+
power = 2
|
|
100
|
+
while power < to:
|
|
101
|
+
power <<= 1
|
|
102
|
+
# while ((number = number_mm () & (power - 1)) >= to);
|
|
103
|
+
number = number_mm() & (power - 1)
|
|
104
|
+
while number >= to:
|
|
105
|
+
number = number_mm() & (power - 1)
|
|
106
|
+
return a + number
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def number_bits(width: int) -> int:
|
|
110
|
+
if width <= 0:
|
|
111
|
+
return 0
|
|
112
|
+
return number_mm() & ((1 << width) - 1)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def dice(number: int, size: int) -> int:
|
|
116
|
+
if size == 0:
|
|
117
|
+
return 0
|
|
118
|
+
if size == 1:
|
|
119
|
+
return number
|
|
120
|
+
total = 0
|
|
121
|
+
for _ in range(number):
|
|
122
|
+
total += number_range(1, size)
|
|
123
|
+
return total
|