rom24-quickmud-python 2.9.21__py3-none-any.whl → 2.13.30__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/account/__init__.py +10 -10
- mud/account/account_manager.py +9 -4
- mud/account/account_service.py +12 -4
- mud/admin_logging/admin.py +2 -2
- mud/advancement.py +13 -7
- mud/affects/engine.py +10 -5
- mud/affects/saves.py +0 -1
- mud/ai/__init__.py +16 -12
- mud/ai/aggressive.py +6 -24
- mud/characters/conditions.py +2 -10
- mud/characters/follow.py +25 -17
- mud/combat/assist.py +6 -29
- mud/combat/death.py +52 -23
- mud/combat/engine.py +379 -129
- mud/combat/messages.py +34 -15
- mud/combat/safety.py +16 -35
- mud/commands/admin_commands.py +15 -42
- mud/commands/advancement.py +64 -58
- mud/commands/affects.py +0 -1
- mud/commands/build.py +15 -9
- mud/commands/channels.py +8 -8
- mud/commands/combat.py +246 -76
- mud/commands/communication.py +99 -51
- mud/commands/compare.py +5 -6
- mud/commands/consider.py +12 -11
- mud/commands/consumption.py +27 -32
- mud/commands/dispatcher.py +251 -39
- mud/commands/doors.py +87 -7
- mud/commands/equipment.py +109 -42
- mud/commands/feedback.py +2 -3
- mud/commands/give.py +35 -31
- mud/commands/group_commands.py +100 -76
- mud/commands/healer.py +15 -8
- mud/commands/help.py +2 -2
- mud/commands/imc.py +6 -6
- mud/commands/imm_admin.py +2 -5
- mud/commands/imm_commands.py +142 -29
- mud/commands/imm_display.py +35 -11
- mud/commands/imm_emote.py +2 -5
- mud/commands/imm_load.py +187 -114
- mud/commands/imm_olc.py +30 -29
- mud/commands/imm_punish.py +2 -5
- mud/commands/imm_search.py +58 -17
- mud/commands/imm_server.py +12 -10
- mud/commands/imm_set.py +53 -36
- mud/commands/info.py +10 -11
- mud/commands/info_extended.py +3 -3
- mud/commands/inspection.py +11 -6
- mud/commands/inventory.py +35 -29
- mud/commands/liquids.py +43 -41
- mud/commands/magic_items.py +46 -62
- mud/commands/misc_info.py +22 -15
- mud/commands/misc_player.py +83 -81
- mud/commands/mobprog_tools.py +2 -2
- mud/commands/movement.py +1 -1
- mud/commands/murder.py +22 -21
- mud/commands/notes.py +4 -11
- mud/commands/obj_manipulation.py +82 -61
- mud/commands/player_config.py +54 -47
- mud/commands/player_info.py +0 -2
- mud/commands/position.py +10 -16
- mud/commands/remaining_rom.py +8 -21
- mud/commands/session.py +29 -15
- mud/commands/shop.py +197 -174
- mud/commands/socials.py +51 -28
- mud/commands/thief_skills.py +61 -66
- mud/commands/typo_guards.py +1 -0
- mud/config.py +0 -3
- mud/db/models.py +4 -4
- mud/db/serializers.py +127 -25
- mud/diagnostics/__init__.py +0 -0
- mud/diagnostics/invariants.py +75 -0
- mud/game_loop.py +330 -122
- mud/groups/xp.py +26 -7
- mud/handler.py +167 -285
- mud/imc/__init__.py +54 -76
- mud/imc/commands.py +9 -8
- mud/loaders/__init__.py +3 -3
- mud/loaders/json_loader.py +30 -8
- mud/loaders/mob_loader.py +15 -6
- mud/loaders/reset_loader.py +3 -9
- mud/loaders/room_loader.py +34 -3
- mud/loaders/specials_loader.py +2 -2
- mud/magic/effects.py +13 -14
- mud/math/stat_apps.py +103 -113
- mud/mob_cmds.py +26 -26
- mud/mobprog.py +182 -18
- mud/models/board.py +1 -3
- mud/models/character.py +250 -112
- mud/models/classes.py +2 -3
- mud/models/constants.py +125 -57
- mud/models/help.py +2 -3
- mud/models/object.py +26 -3
- mud/models/races.py +10 -24
- mud/models/room.py +43 -16
- mud/music/__init__.py +19 -1
- mud/net/ansi.py +9 -9
- mud/net/connection.py +76 -2
- mud/net/protocol.py +20 -7
- mud/net/session.py +1 -6
- mud/network/websocket_server.py +1 -2
- mud/notes.py +2 -2
- mud/registry.py +6 -7
- mud/scripts/convert_are_to_json.py +1 -4
- mud/scripts/convert_skills_to_json.py +20 -2
- mud/skills/groups.py +8 -10
- mud/skills/handlers.py +724 -408
- mud/skills/metadata.py +0 -1
- mud/skills/registry.py +6 -1
- mud/skills/say_spell.py +4 -3
- mud/spawning/reset_handler.py +16 -9
- mud/spawning/templates.py +255 -71
- mud/spec_funs.py +48 -40
- mud/utils/act.py +281 -13
- mud/utils/messaging.py +74 -0
- mud/utils/string_editor.py +2 -12
- mud/utils/timing.py +23 -0
- mud/wiznet.py +8 -1
- mud/world/char_find.py +59 -27
- mud/world/look.py +53 -14
- mud/world/movement.py +72 -37
- mud/world/obj_find.py +5 -5
- mud/world/time_persistence.py +2 -2
- mud/world/vision.py +29 -35
- mud/world/world_state.py +1 -1
- {rom24_quickmud_python-2.9.21.dist-info → rom24_quickmud_python-2.13.30.dist-info}/METADATA +61 -27
- rom24_quickmud_python-2.13.30.dist-info/RECORD +226 -0
- mud/loaders/json_area_loader.py +0 -244
- rom24_quickmud_python-2.9.21.dist-info/RECORD +0 -223
- {rom24_quickmud_python-2.9.21.dist-info → rom24_quickmud_python-2.13.30.dist-info}/WHEEL +0 -0
- {rom24_quickmud_python-2.9.21.dist-info → rom24_quickmud_python-2.13.30.dist-info}/entry_points.txt +0 -0
- {rom24_quickmud_python-2.9.21.dist-info → rom24_quickmud_python-2.13.30.dist-info}/licenses/LICENSE +0 -0
- {rom24_quickmud_python-2.9.21.dist-info → rom24_quickmud_python-2.13.30.dist-info}/top_level.txt +0 -0
mud/account/__init__.py
CHANGED
|
@@ -7,35 +7,35 @@ their passwords directly, mirroring ROM src/merc.h PCData.pwd.
|
|
|
7
7
|
from .account_manager import load_character, save_character
|
|
8
8
|
from .account_service import (
|
|
9
9
|
CreationSelection,
|
|
10
|
+
LoginFailureReason,
|
|
11
|
+
LoginResult,
|
|
10
12
|
account_exists,
|
|
11
13
|
character_exists,
|
|
14
|
+
clear_active_accounts,
|
|
12
15
|
create_account,
|
|
13
16
|
create_character,
|
|
14
17
|
create_character_record,
|
|
15
|
-
|
|
18
|
+
finalize_creation_stats,
|
|
16
19
|
get_creation_classes,
|
|
17
20
|
get_creation_races,
|
|
18
21
|
get_hometown_choices,
|
|
19
|
-
get_weapon_choices,
|
|
20
22
|
get_race_archetype,
|
|
23
|
+
get_weapon_choices,
|
|
21
24
|
is_account_active,
|
|
22
25
|
is_valid_account_name,
|
|
23
26
|
is_valid_character_name,
|
|
27
|
+
list_characters,
|
|
28
|
+
login,
|
|
24
29
|
login_character,
|
|
30
|
+
login_with_host,
|
|
25
31
|
lookup_creation_class,
|
|
26
32
|
lookup_creation_race,
|
|
27
33
|
lookup_hometown,
|
|
28
34
|
lookup_weapon_choice,
|
|
29
|
-
LoginFailureReason,
|
|
30
|
-
LoginResult,
|
|
31
|
-
list_characters,
|
|
32
|
-
login,
|
|
33
|
-
login_with_host,
|
|
34
|
-
roll_creation_stats,
|
|
35
|
-
finalize_creation_stats,
|
|
36
|
-
sanitize_account_name,
|
|
37
35
|
release_account,
|
|
38
36
|
release_character,
|
|
37
|
+
roll_creation_stats,
|
|
38
|
+
sanitize_account_name,
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
__all__ = [
|
mud/account/account_manager.py
CHANGED
|
@@ -28,16 +28,16 @@ from mud.db.session import SessionLocal
|
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from sqlalchemy.orm import Session
|
|
31
|
-
from mud.models.character import Character, character_registry, from_orm
|
|
32
|
-
from mud.models.constants import ROOM_VNUM_LIMBO, ROOM_VNUM_TEMPLE
|
|
33
31
|
from mud.db.serializers import (
|
|
34
32
|
_normalize_int_list,
|
|
35
33
|
_serialize_colour_table,
|
|
34
|
+
_serialize_groups,
|
|
36
35
|
_serialize_object,
|
|
37
36
|
_serialize_pet,
|
|
38
37
|
_serialize_skill_map,
|
|
39
|
-
_serialize_groups,
|
|
40
38
|
)
|
|
39
|
+
from mud.models.character import Character, character_registry, from_orm
|
|
40
|
+
from mud.models.constants import ROOM_VNUM_LIMBO, ROOM_VNUM_TEMPLE
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def load_character(char_name: str, _ignored: str | None = None) -> Character | None:
|
|
@@ -265,10 +265,12 @@ def save_character_to_db(session: Session, character: Character) -> None:
|
|
|
265
265
|
|
|
266
266
|
# perm_stats JSON (already stored as string, keep existing column)
|
|
267
267
|
from mud.models.character import _encode_perm_stats
|
|
268
|
+
|
|
268
269
|
db_char.perm_stats = _encode_perm_stats(getattr(character, "perm_stat", []))
|
|
269
270
|
|
|
270
271
|
# creation_groups / creation_skills (keep in sync)
|
|
271
272
|
from mud.models.character import _encode_creation_groups, _encode_creation_skills
|
|
273
|
+
|
|
272
274
|
db_char.creation_groups = _encode_creation_groups(getattr(character, "creation_groups", ()))
|
|
273
275
|
db_char.creation_skills = _encode_creation_skills(getattr(character, "creation_skills", ()))
|
|
274
276
|
db_char.creation_points = int(getattr(character, "creation_points", 0) or 0)
|
|
@@ -312,6 +314,7 @@ def save_character_to_db(session: Session, character: Character) -> None:
|
|
|
312
314
|
def _dataclass_to_dict(obj: object) -> dict:
|
|
313
315
|
"""Recursively convert a dataclass instance to a plain dict for JSON storage."""
|
|
314
316
|
import dataclasses
|
|
317
|
+
|
|
315
318
|
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
316
319
|
result = {}
|
|
317
320
|
for f in dataclasses.fields(obj): # type: ignore[arg-type]
|
|
@@ -320,7 +323,9 @@ def _dataclass_to_dict(obj: object) -> dict:
|
|
|
320
323
|
result[f.name] = _dataclass_to_dict(val)
|
|
321
324
|
elif isinstance(val, list):
|
|
322
325
|
result[f.name] = [
|
|
323
|
-
_dataclass_to_dict(item)
|
|
326
|
+
_dataclass_to_dict(item)
|
|
327
|
+
if (dataclasses.is_dataclass(item) and not isinstance(item, type))
|
|
328
|
+
else item
|
|
324
329
|
for item in val
|
|
325
330
|
]
|
|
326
331
|
elif isinstance(val, dict):
|
mud/account/account_service.py
CHANGED
|
@@ -101,6 +101,7 @@ _WEAPON_CHOICES: Final[dict[str, tuple[str, ...]]] = {
|
|
|
101
101
|
"warrior": ("sword", "mace"),
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
|
|
104
105
|
@lru_cache(maxsize=1)
|
|
105
106
|
def _load_skill_data() -> dict[str, dict[str, object]]:
|
|
106
107
|
"""Return ROM skill metadata (type/ratings) keyed by lower-case name."""
|
|
@@ -152,7 +153,7 @@ def _skill_ratings(name: str) -> tuple[int, int, int, int] | None:
|
|
|
152
153
|
metadata = ROM_SKILL_METADATA.get(normalized)
|
|
153
154
|
if metadata is not None:
|
|
154
155
|
ratings_meta = metadata.get("ratings")
|
|
155
|
-
if isinstance(ratings_meta,
|
|
156
|
+
if isinstance(ratings_meta, list | tuple) and len(ratings_meta) >= 4:
|
|
156
157
|
try:
|
|
157
158
|
return tuple(int(value) for value in ratings_meta[:4])
|
|
158
159
|
except (TypeError, ValueError):
|
|
@@ -433,8 +434,13 @@ class CreationSelection:
|
|
|
433
434
|
return base + 40
|
|
434
435
|
|
|
435
436
|
def train_value(self) -> int:
|
|
436
|
-
|
|
437
|
-
|
|
437
|
+
# mirroring ROM src/nanny.c:776 — CON_READ_MOTD unconditionally sets
|
|
438
|
+
# ch->train = 3 for every level-0 character, overwriting the
|
|
439
|
+
# `ch->train = (40 - points + 1) / 2` formula from src/nanny.c:684.
|
|
440
|
+
# The formula never survives to CON_PLAYING (it is dead code in ROM),
|
|
441
|
+
# so a freshly created PC always starts with exactly 3 training
|
|
442
|
+
# sessions regardless of how many creation points were spent. (practice
|
|
443
|
+
# is likewise hardcoded to 5 at src/nanny.c:777.)
|
|
438
444
|
return 3
|
|
439
445
|
|
|
440
446
|
def group_names(self) -> tuple[str, ...]:
|
|
@@ -957,7 +963,9 @@ def list_characters(
|
|
|
957
963
|
if not name:
|
|
958
964
|
return []
|
|
959
965
|
if require_act_flags is not None:
|
|
960
|
-
required_bits =
|
|
966
|
+
required_bits = (
|
|
967
|
+
int(require_act_flags) if not isinstance(require_act_flags, PlayerFlag) else int(require_act_flags)
|
|
968
|
+
)
|
|
961
969
|
act_flags = int(getattr(account, "act", 0) or 0)
|
|
962
970
|
if act_flags & required_bits != required_bits:
|
|
963
971
|
return []
|
mud/admin_logging/admin.py
CHANGED
|
@@ -80,7 +80,7 @@ def _duplicate_wiznet_chars(text: str) -> str:
|
|
|
80
80
|
return "".join(duplicated)
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
def _get_effective_trust(character:
|
|
83
|
+
def _get_effective_trust(character: Character) -> int:
|
|
84
84
|
"""Mirror ROM's ``get_trust`` helper used for wiznet broadcasts."""
|
|
85
85
|
|
|
86
86
|
trust = getattr(character, "trust", 0)
|
|
@@ -91,7 +91,7 @@ def log_admin_command(
|
|
|
91
91
|
actor: str,
|
|
92
92
|
command_line: str,
|
|
93
93
|
*,
|
|
94
|
-
character:
|
|
94
|
+
character: Character | None = None,
|
|
95
95
|
) -> None:
|
|
96
96
|
"""Append a single admin-command entry to log/admin.log.
|
|
97
97
|
|
mud/advancement.py
CHANGED
|
@@ -11,6 +11,7 @@ from mud.models.constants import LEVEL_HERO
|
|
|
11
11
|
from mud.models.races import PcRaceType, list_playable_races
|
|
12
12
|
from mud.models.titles import default_title_text
|
|
13
13
|
from mud.utils import rng_mm
|
|
14
|
+
from mud.utils.messaging import send_to_char_buffered
|
|
14
15
|
from mud.wiznet import WiznetFlag, wiznet
|
|
15
16
|
|
|
16
17
|
BASE_XP_PER_LEVEL = 1000
|
|
@@ -53,9 +54,7 @@ def exp_per_level(char: Character) -> int:
|
|
|
53
54
|
return _creation_exp_floor(char, BASE_XP_PER_LEVEL)
|
|
54
55
|
|
|
55
56
|
|
|
56
|
-
def exp_per_level_for_creation(
|
|
57
|
-
race: PcRaceType, class_type: ClassType, creation_points: int
|
|
58
|
-
) -> int:
|
|
57
|
+
def exp_per_level_for_creation(race: PcRaceType, class_type: ClassType, creation_points: int) -> int:
|
|
59
58
|
"""ROM-style experience curve based on creation points."""
|
|
60
59
|
|
|
61
60
|
points = max(0, int(creation_points))
|
|
@@ -144,7 +143,7 @@ def advance_level(char: Character) -> None:
|
|
|
144
143
|
|
|
145
144
|
set_title(char, default_title_text(char.ch_class, char.level, getattr(char, "sex", 0)))
|
|
146
145
|
|
|
147
|
-
if
|
|
146
|
+
if not getattr(char, "is_npc", False):
|
|
148
147
|
hit_suffix = "" if hp == 1 else "s"
|
|
149
148
|
practice_suffix = "" if practice_gain == 1 else "s"
|
|
150
149
|
message = (
|
|
@@ -152,7 +151,11 @@ def advance_level(char: Character) -> None:
|
|
|
152
151
|
f"{hp} hit point{hit_suffix}, {mana} mana, {move} move, and "
|
|
153
152
|
f"{practice_gain} practice{practice_suffix}.{ROM_NEWLINE}"
|
|
154
153
|
)
|
|
155
|
-
|
|
154
|
+
# mirroring ROM src/update.c:113 advance_level — `send_to_char(buf, ch)`
|
|
155
|
+
# straight to the descriptor. advance_level fires from gain_exp during a
|
|
156
|
+
# combat-tick kill; route through the async-aware helper so a leveling PC
|
|
157
|
+
# sees it immediately, not at the next command (mailbox-strand bug).
|
|
158
|
+
send_to_char_buffered(char, message)
|
|
156
159
|
|
|
157
160
|
|
|
158
161
|
def _creation_exp_floor(char: Character, fallback: int) -> int:
|
|
@@ -217,8 +220,10 @@ def gain_exp(char: Character, amount: int) -> None:
|
|
|
217
220
|
|
|
218
221
|
# Level up while total exp meets threshold for next level.
|
|
219
222
|
while char.level < LEVEL_HERO and char.exp >= exp_per_level(char) * (char.level + 1):
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
# mirroring ROM src/update.c:131 gain_exp — `send_to_char(...)` straight
|
|
224
|
+
# to the descriptor during the combat tick; async-aware delivery so the
|
|
225
|
+
# ding shows at kill time, not at the player's next command.
|
|
226
|
+
send_to_char_buffered(char, "{GYou raise a level!! {x")
|
|
222
227
|
char.level += 1
|
|
223
228
|
log_game_event(f"{getattr(char, 'name', 'Someone')} gained level {char.level}")
|
|
224
229
|
wiznet(
|
|
@@ -232,4 +237,5 @@ def gain_exp(char: Character, amount: int) -> None:
|
|
|
232
237
|
advance_level(char)
|
|
233
238
|
# Lazy import to avoid circular dependency
|
|
234
239
|
from mud.account.account_manager import save_character
|
|
240
|
+
|
|
235
241
|
save_character(char)
|
mud/affects/engine.py
CHANGED
|
@@ -56,9 +56,16 @@ def tick_spell_effects(character: Character) -> list[str]:
|
|
|
56
56
|
duration = int(getattr(affect, "duration", 0) or 0)
|
|
57
57
|
if duration > 0:
|
|
58
58
|
affect.duration = duration - 1
|
|
59
|
+
# ROM src/update.c:768 — `if (number_range(0,4) == 0 && paf->level > 0)`.
|
|
60
|
+
# C `&&` is left-to-right short-circuit and number_range advances MM
|
|
61
|
+
# state as a side effect, so the roll is consumed UNCONDITIONALLY for
|
|
62
|
+
# every duration>0 affect; `level > 0` is only tested afterwards. The
|
|
63
|
+
# operands must NOT be swapped (`level > 0 and number_range(...)` skips
|
|
64
|
+
# the roll at level 0, desyncing the global RNG stream — GL-026).
|
|
65
|
+
fades = rng_mm.number_range(0, 4) == 0
|
|
59
66
|
level = int(getattr(affect, "level", 0) or 0)
|
|
60
|
-
if
|
|
61
|
-
affect.level = level - 1
|
|
67
|
+
if fades and level > 0:
|
|
68
|
+
affect.level = level - 1
|
|
62
69
|
spell_name = getattr(affect, "type", None)
|
|
63
70
|
if isinstance(spell_name, str) and spell_name in effects:
|
|
64
71
|
touched_names.add(spell_name)
|
|
@@ -114,9 +121,7 @@ def tick_spell_effects(character: Character) -> list[str]:
|
|
|
114
121
|
|
|
115
122
|
for spell_name in touched_names:
|
|
116
123
|
remaining = [
|
|
117
|
-
affect
|
|
118
|
-
for affect in getattr(character, "affected", [])
|
|
119
|
-
if getattr(affect, "type", None) == spell_name
|
|
124
|
+
affect for affect in getattr(character, "affected", []) if getattr(affect, "type", None) == spell_name
|
|
120
125
|
]
|
|
121
126
|
if remaining:
|
|
122
127
|
primary = remaining[0]
|
mud/affects/saves.py
CHANGED
mud/ai/__init__.py
CHANGED
|
@@ -170,18 +170,22 @@ def _take_object(mob: Character, obj: Object) -> None:
|
|
|
170
170
|
obj.location = None
|
|
171
171
|
if hasattr(obj, "in_obj"):
|
|
172
172
|
obj.in_obj = None
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
173
|
+
# obj.carried_by is set by add_object / add_to_inventory below
|
|
174
|
+
|
|
175
|
+
# INV-039 / class-13: route through head-insert chokepoint.
|
|
176
|
+
# Character.add_object / MobInstance.add_to_inventory handles carry_number
|
|
177
|
+
# and carry_weight internally — no manual counter update needed.
|
|
178
|
+
add_obj = getattr(mob, "add_object", None)
|
|
179
|
+
if callable(add_obj):
|
|
180
|
+
add_obj(obj)
|
|
181
|
+
else:
|
|
182
|
+
add_inv = getattr(mob, "add_to_inventory", None)
|
|
183
|
+
if callable(add_inv):
|
|
184
|
+
add_inv(obj)
|
|
185
|
+
else:
|
|
186
|
+
inventory = getattr(mob, "inventory", None)
|
|
187
|
+
if isinstance(inventory, list) and obj not in inventory:
|
|
188
|
+
inventory.insert(0, obj)
|
|
185
189
|
|
|
186
190
|
room = getattr(mob, "room", None)
|
|
187
191
|
if room is not None and hasattr(room, "broadcast"):
|
mud/ai/aggressive.py
CHANGED
|
@@ -6,7 +6,7 @@ from collections.abc import Iterable
|
|
|
6
6
|
|
|
7
7
|
from mud.combat import multi_hit
|
|
8
8
|
from mud.models.character import Character, character_registry
|
|
9
|
-
from mud.models.constants import ActFlag, AffectFlag,
|
|
9
|
+
from mud.models.constants import LEVEL_IMMORTAL, ActFlag, AffectFlag, RoomFlag
|
|
10
10
|
from mud.utils import rng_mm
|
|
11
11
|
|
|
12
12
|
|
|
@@ -17,24 +17,6 @@ def _has_flag(value: int, flag: ActFlag) -> bool:
|
|
|
17
17
|
return False
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def _has_affect(ch: Character, flag: AffectFlag) -> bool:
|
|
21
|
-
checker = getattr(ch, "has_affect", None)
|
|
22
|
-
if callable(checker):
|
|
23
|
-
return bool(checker(flag))
|
|
24
|
-
affected = getattr(ch, "affected_by", 0)
|
|
25
|
-
try:
|
|
26
|
-
return bool(int(affected) & int(flag))
|
|
27
|
-
except Exception:
|
|
28
|
-
return False
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _is_awake(ch: Character) -> bool:
|
|
32
|
-
checker = getattr(ch, "is_awake", None)
|
|
33
|
-
if callable(checker):
|
|
34
|
-
return bool(checker())
|
|
35
|
-
return int(getattr(ch, "position", 0)) > int(Position.SLEEPING)
|
|
36
|
-
|
|
37
|
-
|
|
38
20
|
def _can_see(attacker: Character, target: Character | None) -> bool:
|
|
39
21
|
if attacker is None or target is None:
|
|
40
22
|
return False
|
|
@@ -62,7 +44,7 @@ def _eligible_victims(ch: Character, occupants: Iterable[Character]) -> Iterable
|
|
|
62
44
|
continue
|
|
63
45
|
if int(getattr(ch, "level", 0)) < int(getattr(candidate, "level", 0)) - 5:
|
|
64
46
|
continue
|
|
65
|
-
if _has_flag(getattr(ch, "act", 0), ActFlag.WIMPY) and
|
|
47
|
+
if _has_flag(getattr(ch, "act", 0), ActFlag.WIMPY) and candidate.is_awake():
|
|
66
48
|
continue
|
|
67
49
|
if not _can_see(ch, candidate):
|
|
68
50
|
continue
|
|
@@ -91,15 +73,15 @@ def aggressive_update() -> None:
|
|
|
91
73
|
continue
|
|
92
74
|
if int(getattr(room, "room_flags", 0)) & int(RoomFlag.ROOM_SAFE):
|
|
93
75
|
continue
|
|
94
|
-
if
|
|
76
|
+
if mob.has_affect(AffectFlag.CALM):
|
|
95
77
|
continue
|
|
96
78
|
if getattr(mob, "fighting", None) is not None:
|
|
97
79
|
continue
|
|
98
|
-
if
|
|
80
|
+
if mob.has_affect(AffectFlag.CHARM):
|
|
99
81
|
continue
|
|
100
|
-
if not
|
|
82
|
+
if not mob.is_awake():
|
|
101
83
|
continue
|
|
102
|
-
if _has_flag(getattr(mob, "act", 0), ActFlag.WIMPY) and
|
|
84
|
+
if _has_flag(getattr(mob, "act", 0), ActFlag.WIMPY) and watcher.is_awake():
|
|
103
85
|
continue
|
|
104
86
|
if not _can_see(mob, watcher):
|
|
105
87
|
continue
|
mud/characters/conditions.py
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from mud.models.character import Character
|
|
4
|
-
from mud.models.constants import
|
|
5
|
-
|
|
4
|
+
from mud.models.constants import LEVEL_IMMORTAL, Condition
|
|
5
|
+
from mud.utils.messaging import send_to_char_buffered as _send_to_char
|
|
6
6
|
|
|
7
7
|
__all__ = ["gain_condition"]
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def _send_to_char(character: Character, message: str) -> None:
|
|
11
|
-
"""Append a message to the character if a buffer is available."""
|
|
12
|
-
|
|
13
|
-
messages = getattr(character, "messages", None)
|
|
14
|
-
if isinstance(messages, list):
|
|
15
|
-
messages.append(message)
|
|
16
|
-
|
|
17
|
-
|
|
18
10
|
def gain_condition(character: Character, condition: Condition, delta: int) -> None:
|
|
19
11
|
"""Adjust a player's condition slot, mirroring ROM gain_condition."""
|
|
20
12
|
|
mud/characters/follow.py
CHANGED
|
@@ -3,12 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from mud.models.constants import AffectFlag
|
|
6
|
+
from mud.utils.messaging import push_message
|
|
6
7
|
|
|
7
8
|
if TYPE_CHECKING: # pragma: no cover - import for type checkers only
|
|
8
9
|
from mud.models.character import Character
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
def _display_name(character:
|
|
12
|
+
def _display_name(character: Character | None) -> str:
|
|
12
13
|
if character is None:
|
|
13
14
|
return "Someone"
|
|
14
15
|
name = getattr(character, "name", None)
|
|
@@ -20,8 +21,10 @@ def _display_name(character: "Character" | None) -> str:
|
|
|
20
21
|
return "Someone"
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
def add_follower(follower:
|
|
24
|
+
def add_follower(follower: Character, master: Character) -> None:
|
|
24
25
|
"""Attach ``follower`` to ``master`` mirroring ROM ``add_follower``."""
|
|
26
|
+
# mirroring ROM src/act_comm.c:1591-1607
|
|
27
|
+
from mud.world.vision import can_see_character
|
|
25
28
|
|
|
26
29
|
if getattr(follower, "master", None) is master:
|
|
27
30
|
return
|
|
@@ -31,17 +34,20 @@ def add_follower(follower: "Character", master: "Character") -> None:
|
|
|
31
34
|
follower.master = master
|
|
32
35
|
follower.leader = None
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
if
|
|
36
|
-
|
|
37
|
+
# ROM lines 1602-1603: TO_VICT gated on can_see(master, ch).
|
|
38
|
+
if can_see_character(master, follower):
|
|
39
|
+
# mirroring ROM src/act_comm.c:1602-1603 — act(..., TO_VICT)
|
|
40
|
+
# writes immediately to the descriptor; mailbox is fallback only.
|
|
41
|
+
push_message(master, f"{_display_name(follower)} now follows you.")
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
follower_messages.append(f"You now follow {_display_name(master)}.")
|
|
43
|
+
# ROM line 1605: TO_CHAR is unconditional.
|
|
44
|
+
push_message(follower, f"You now follow {_display_name(master)}.")
|
|
41
45
|
|
|
42
46
|
|
|
43
|
-
def stop_follower(follower:
|
|
47
|
+
def stop_follower(follower: Character) -> None:
|
|
44
48
|
"""Detach ``follower`` from its master and clear charm effects."""
|
|
49
|
+
# mirroring ROM src/act_comm.c:1612-1636
|
|
50
|
+
from mud.world.vision import can_see_character
|
|
45
51
|
|
|
46
52
|
master = getattr(follower, "master", None)
|
|
47
53
|
if master is None:
|
|
@@ -52,13 +58,15 @@ def stop_follower(follower: "Character") -> None:
|
|
|
52
58
|
elif follower.has_affect(AffectFlag.CHARM):
|
|
53
59
|
follower.remove_affect(AffectFlag.CHARM)
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
# ROM lines 1625-1629: both messages gated on
|
|
62
|
+
# can_see(ch->master, ch) && ch->in_room != NULL.
|
|
63
|
+
if can_see_character(master, follower) and getattr(follower, "room", None) is not None:
|
|
64
|
+
# ROM src/act_comm.c:1626-1627 — act(..., TO_VICT)/act(..., TO_CHAR) write
|
|
65
|
+
# immediately to the descriptor; push_message routes a connected PC to the
|
|
66
|
+
# async send and falls back to the mailbox only for disconnected chars
|
|
67
|
+
# (matching add_follower above — ACT_COMM-003 / INV-001 wrong-channel).
|
|
68
|
+
push_message(master, f"{_display_name(follower)} stops following you.")
|
|
69
|
+
push_message(follower, f"You stop following {_display_name(master)}.")
|
|
62
70
|
|
|
63
71
|
if getattr(master, "pet", None) is follower:
|
|
64
72
|
master.pet = None
|
|
@@ -67,7 +75,7 @@ def stop_follower(follower: "Character") -> None:
|
|
|
67
75
|
follower.leader = None
|
|
68
76
|
|
|
69
77
|
|
|
70
|
-
def die_follower(char:
|
|
78
|
+
def die_follower(char: Character) -> None:
|
|
71
79
|
"""Detach a dying character from its group and followers.
|
|
72
80
|
|
|
73
81
|
Mirrors ROM ``src/act_comm.c:1658-1680``:
|
mud/combat/assist.py
CHANGED
|
@@ -6,7 +6,6 @@ ROM Reference: src/fight.c check_assist (lines 105-181)
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import asyncio
|
|
10
9
|
from typing import TYPE_CHECKING
|
|
11
10
|
|
|
12
11
|
from mud.characters import is_same_group
|
|
@@ -56,7 +55,7 @@ def check_assist(ch: Character, victim: Character) -> None:
|
|
|
56
55
|
# ROM uses rch_next pattern to handle this
|
|
57
56
|
for rch in list(people_in_room): # Create a copy to avoid modification issues
|
|
58
57
|
# Skip if not awake or already fighting
|
|
59
|
-
if not
|
|
58
|
+
if not rch.is_awake():
|
|
60
59
|
continue
|
|
61
60
|
|
|
62
61
|
if getattr(rch, "fighting", None) is not None:
|
|
@@ -151,14 +150,6 @@ def check_assist(ch: Character, victim: Character) -> None:
|
|
|
151
150
|
multi_hit(rch, target, None)
|
|
152
151
|
|
|
153
152
|
|
|
154
|
-
def _is_awake(char: Character) -> bool:
|
|
155
|
-
"""Check if character is awake (ROM IS_AWAKE macro)."""
|
|
156
|
-
from mud.models.constants import Position
|
|
157
|
-
|
|
158
|
-
position = getattr(char, "position", Position.STANDING)
|
|
159
|
-
return position > Position.SLEEPING
|
|
160
|
-
|
|
161
|
-
|
|
162
153
|
def _is_npc(char: Character) -> bool:
|
|
163
154
|
"""Check if character is NPC (ROM IS_NPC macro)."""
|
|
164
155
|
return getattr(char, "is_npc", False)
|
|
@@ -190,22 +181,8 @@ def _emote(char: Character, message: str) -> None:
|
|
|
190
181
|
people = getattr(room, "people", [])
|
|
191
182
|
for person in people:
|
|
192
183
|
if person != char:
|
|
193
|
-
_send_to_char(person, full_message)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
# Direct delivery for connected characters
|
|
199
|
-
writer = getattr(char, "connection", None)
|
|
200
|
-
if writer is not None:
|
|
201
|
-
from mud.net.protocol import send_to_char as _send
|
|
202
|
-
|
|
203
|
-
asyncio.create_task(_send(char, message + "\n"))
|
|
204
|
-
# Queue fallback for tests
|
|
205
|
-
send = getattr(char, "send", None)
|
|
206
|
-
if callable(send):
|
|
207
|
-
send(message + "\n")
|
|
208
|
-
else:
|
|
209
|
-
messages = getattr(char, "messages", None)
|
|
210
|
-
if isinstance(messages, list):
|
|
211
|
-
messages.append(message + "\n")
|
|
184
|
+
_send_to_char(person, full_message + "\n")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# DUPL-001b — canonical at mud/utils/messaging.py:send_to_char_buffered.
|
|
188
|
+
from mud.utils.messaging import send_to_char_buffered as _send_to_char # noqa: E402
|