rom24-quickmud-python 2.14.264__py3-none-any.whl → 2.14.298__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.
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import UTC, datetime
3
+ from datetime import datetime, timezone
4
4
  from pathlib import Path
5
5
  from typing import TYPE_CHECKING
6
6
 
@@ -100,7 +100,7 @@ def log_admin_command(
100
100
  """
101
101
 
102
102
  Path("log").mkdir(exist_ok=True)
103
- timestamp = datetime.now(UTC).isoformat().replace("+00:00", "Z")
103
+ timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
104
104
  sanitized = _sanitize_command_line(command_line)
105
105
  if character is not None:
106
106
  try:
@@ -132,7 +132,7 @@ def rotate_admin_log(today: datetime | None = None) -> Path:
132
132
  active = log_dir / "admin.log"
133
133
  if not active.exists():
134
134
  return active
135
- dt = today or datetime.now(UTC)
135
+ dt = today or datetime.now(timezone.utc)
136
136
  dated = log_dir / f"admin-{dt.strftime('%Y%m%d')}.log"
137
137
  # Avoid clobbering: if dated file exists, append current log and remove active
138
138
  if dated.exists():
mud/combat/engine.py CHANGED
@@ -688,6 +688,25 @@ def apply_damage(
688
688
  if victim.position == Position.DEAD:
689
689
  return "Already dead."
690
690
 
691
+ # FIGHT-093 — "Stop up any residual loopholes." (ROM src/fight.c:697-713).
692
+ # A physical hit (dt >= TYPE_HIT) dealing more than 1200 raw damage is clamped
693
+ # to 1200; a non-immortal attacker is penalized with "You really shouldn't
694
+ # cheat." and has their wielded weapon extracted from the game (extract_obj).
695
+ # This runs before the >35/>80 reduction, exactly as in ROM.
696
+ if damage > 1200 and _should_check_weapon_defenses(dt):
697
+ damage = 1200
698
+ is_imm = getattr(attacker, "is_immortal", None)
699
+ attacker_immortal = bool(is_imm()) if callable(is_imm) else False
700
+ if not attacker_immortal:
701
+ # mirroring ROM src/fight.c:707-711 — get_eq_char(ch, WEAR_WIELD) then
702
+ # send "You really shouldn't cheat." and extract_obj on the weapon.
703
+ weapon = get_wielded_weapon(attacker)
704
+ _push_message(attacker, "You really shouldn't cheat.")
705
+ if weapon is not None:
706
+ from mud.game_loop import _extract_obj
707
+
708
+ _extract_obj(weapon)
709
+
691
710
  # mirroring ROM src/fight.c:717-720 — damage soft-cap applied before all other
692
711
  # modifiers; reduces raw spikes to prevent instant-kill from single high-damage hits.
693
712
  if damage > 35:
mud/commands/character.py CHANGED
@@ -38,7 +38,8 @@ def do_password(ch: Character, args: str) -> str:
38
38
  parts = args.split(None, 1)
39
39
 
40
40
  if len(parts) < 2:
41
- return "Syntax: password <old> <new>"
41
+ # PASSWORD-002: ROM src/act_info.c:2889 — trailing period.
42
+ return "Syntax: password <old> <new>."
42
43
 
43
44
  old_password = parts[0]
44
45
  new_password = parts[1]
@@ -57,7 +58,8 @@ def do_password(ch: Character, args: str) -> str:
57
58
  # The macro is UMAX(ch->wait, 40); a plain `= 40` would lower a higher
58
59
  # existing wait. Use the canonical UMAX helper.
59
60
  apply_wait_state(ch, 40)
60
- return "Wrong password. Wait 10 seconds."
61
+ # PASSWORD-002: ROM src/act_info.c:2896 TWO spaces after "password.".
62
+ return "Wrong password. Wait 10 seconds."
61
63
 
62
64
  # Gap 1 (P1): Check minimum length (ROM src/act_info.c:2897-2904)
63
65
  if len(new_password) < 5:
mud/commands/combat.py CHANGED
@@ -11,6 +11,7 @@ from mud.models.character import Character
11
11
  from mud.models.constants import (
12
12
  AC_BASH,
13
13
  EX_CLOSED,
14
+ LEVEL_HERO,
14
15
  LEVEL_IMMORTAL,
15
16
  ActFlag,
16
17
  AffectFlag,
@@ -45,6 +46,9 @@ _FLEE_MOVEMENT_LOSS: dict[Sector, int] = {
45
46
  Sector.DESERT: 6,
46
47
  }
47
48
 
49
+ # mirroring ROM src/act_move.c dir_name[] — indexed by door number (0..5).
50
+ _FLEE_DIR_NAMES: tuple[str, ...] = ("north", "east", "south", "west", "up", "down")
51
+
48
52
 
49
53
  def _kill_safety_message(attacker: Character, victim: Character) -> str | None:
50
54
  # FIGHT-074: pure mirror of ROM's is_safe() (src/fight.c:1059) — which contains
@@ -171,10 +175,10 @@ def do_kill(char: Character, args: str) -> str:
171
175
 
172
176
 
173
177
  def do_kick(char: Character, args: str) -> str:
174
- opponent = getattr(char, "fighting", None)
175
- if opponent is None:
176
- return "You aren't fighting anyone."
177
-
178
+ # KICK-001: ROM do_kick (src/fight.c:3109-3124) checks the PC level gate and
179
+ # NPC OFF_KICK gate BEFORE the `fighting == NULL` check, so a sub-level PC who
180
+ # is not fighting sees "You better leave the martial arts to fighters.", not
181
+ # "You aren't fighting anyone." The `fighting is None` check is relocated below.
178
182
  try:
179
183
  skill = skill_registry.get("kick")
180
184
  except KeyError:
@@ -204,6 +208,12 @@ def do_kick(char: Character, args: str) -> str:
204
208
  if required_level is not None and getattr(char, "level", 0) < required_level:
205
209
  return "You better leave the martial arts to fighters."
206
210
 
211
+ # KICK-001: ROM src/fight.c:3121-3124 — `fighting == NULL` check comes AFTER
212
+ # the level/OFF_KICK gates.
213
+ opponent = getattr(char, "fighting", None)
214
+ if opponent is None:
215
+ return "You aren't fighting anyone."
216
+
207
217
  if skill is not None:
208
218
  if int(getattr(char, "wait", 0) or 0) > 0:
209
219
  # INV-001 SINGLE-DELIVERY: return-channel only; a mailbox append would
@@ -815,8 +825,20 @@ def do_flee(char: Character, args: str) -> str:
815
825
  continue # too exhausted — mirrors move_char returning without moving
816
826
  flee_move_cost = cost
817
827
 
828
+ # MOVE-009: ROM src/fight.c:3002 flees via move_char(ch, door, FALSE),
829
+ # which broadcasts "$n leaves $T." to the fled-from room and "$n has
830
+ # arrived." to the destination (src/act_move.c:196-202) — suppressed for
831
+ # a sneaking or wizinvis fleer. The inline flee move omitted both.
832
+ show_movement = not (
833
+ char.has_affect(AffectFlag.SNEAK) or int(getattr(char, "invis_level", 0) or 0) >= LEVEL_HERO
834
+ )
835
+ dir_name = _FLEE_DIR_NAMES[door] if door < len(_FLEE_DIR_NAMES) else ""
836
+ if show_movement:
837
+ act_to_room(was_in, "$n leaves $T.", char, arg2=dir_name, exclude=char)
818
838
  was_in.remove_character(char)
819
839
  new_room.add_character(char)
840
+ if show_movement:
841
+ act_to_room(new_room, "$n has arrived.", char, exclude=char)
820
842
  now_in = new_room
821
843
  break
822
844
 
@@ -1202,7 +1224,8 @@ def do_trip(char: Character, args: str, *, victim: Character | None = None) -> s
1202
1224
  else:
1203
1225
  skill_level = _character_skill_percent(char, "trip")
1204
1226
  if skill_level == 0:
1205
- return "Tripping? What's that?"
1227
+ # TRIP-001: ROM src/fight.c:2654 — TWO spaces after "Tripping?".
1228
+ return "Tripping? What's that?"
1206
1229
 
1207
1230
  # Find target (skip when an explicit victim is supplied via delegation).
1208
1231
  if victim is not None:
mud/commands/compare.py CHANGED
@@ -38,7 +38,10 @@ def do_compare(char: Character, args: str) -> str:
38
38
  # Compare two items
39
39
  obj2 = get_obj_carry(char, parts[1])
40
40
  if obj2 is None:
41
- return "You do not have that second item."
41
+ # COMPARE-002: ROM src/act_info.c:2338-2341 emits the SAME string as
42
+ # the missing-first-item branch (:2317) — not a distinct "second item"
43
+ # variant. The faithful port must reuse "You do not have that item.".
44
+ return "You do not have that item."
42
45
  else:
43
46
  # Compare against equipped item of same type
44
47
  obj2 = _find_equipped_match(char, obj1)
mud/commands/doors.py CHANGED
@@ -378,11 +378,14 @@ def do_lock(char: Character, args: str) -> str:
378
378
 
379
379
  # Container (ROM C lines 627-656)
380
380
  if item_type == ItemType.CONTAINER:
381
- if not (values[1] & ContainerFlag.CLOSEABLE):
382
- return "You can't do that."
381
+ # LOCK-001: ROM do_lock's container arm has NO CONT_CLOSEABLE check
382
+ # (only do_open/do_close do — MOVE-008). Its first guard is CONT_CLOSED.
383
383
  if not (values[1] & ContainerFlag.CLOSED):
384
384
  return "It's not closed."
385
- if values[2] <= 0: # No key defined
385
+ # LOCK-002: ROM src/act_move.c:637 gates on `value[2] < 0`. Key vnum 0
386
+ # (a keyless container) falls through to has_key → "You lack the key.",
387
+ # matching the portal sibling arm above (which already uses `< 0`).
388
+ if values[2] < 0:
386
389
  return "It can't be locked."
387
390
  if not _has_key(char, values[2]):
388
391
  return "You lack the key."
@@ -483,11 +486,12 @@ def do_unlock(char: Character, args: str) -> str:
483
486
 
484
487
  # Container (ROM C lines 761-791)
485
488
  if item_type == ItemType.CONTAINER:
486
- if not (values[1] & ContainerFlag.CLOSEABLE):
487
- return "You can't do that."
489
+ # LOCK-001: ROM do_unlock's container arm has NO CONT_CLOSEABLE check
490
+ # (only do_open/do_close do — MOVE-008). Its first guard is CONT_CLOSED.
488
491
  if not (values[1] & ContainerFlag.CLOSED):
489
492
  return "It's not closed."
490
- if values[2] <= 0: # No key defined
493
+ # LOCK-002: ROM src/act_move.c:773 gates on `value[2] < 0` (see do_lock).
494
+ if values[2] < 0:
491
495
  return "It can't be unlocked."
492
496
  if not _has_key(char, values[2]):
493
497
  return "You lack the key."
mud/commands/equipment.py CHANGED
@@ -18,6 +18,25 @@ if TYPE_CHECKING:
18
18
  from mud.models.object import Object
19
19
 
20
20
 
21
+ # WEAR-015: wear-location flags checked BEFORE the HOLD branch in ROM `wear_obj`
22
+ # (every bit below HOLD's 1<<14 except WIELD, which Python dispatches by
23
+ # item_type). An object carrying any of these takes the armor/shield slot, not HOLD.
24
+ _PRE_HOLD_WEAR_MASK = (
25
+ WearFlag.WEAR_FINGER
26
+ | WearFlag.WEAR_NECK
27
+ | WearFlag.WEAR_BODY
28
+ | WearFlag.WEAR_HEAD
29
+ | WearFlag.WEAR_LEGS
30
+ | WearFlag.WEAR_FEET
31
+ | WearFlag.WEAR_HANDS
32
+ | WearFlag.WEAR_ARMS
33
+ | WearFlag.WEAR_SHIELD
34
+ | WearFlag.WEAR_ABOUT
35
+ | WearFlag.WEAR_WAIST
36
+ | WearFlag.WEAR_WRIST
37
+ )
38
+
39
+
21
40
  # ROM str_app wield column for STR 0..25.
22
41
  # Source: src/const.c:728 str_app[26], fourth field (wield).
23
42
  # Max wieldable weight = _STR_WIELD[STR] * 10 (ROM src/act_obj.c:1624-1625).
@@ -141,6 +160,35 @@ def _can_wear_alignment(ch: Character, obj: Object) -> tuple[bool, str | None]:
141
160
  return True, None
142
161
 
143
162
 
163
+ def _zap_align(ch: Character, obj: Object) -> str:
164
+ """ROM `equip_char` alignment zap — WEAR-014 (src/handler.c:1765-1777).
165
+
166
+ When an evil PC dons an ITEM_ANTI_EVIL object (or good+ANTI_GOOD /
167
+ neutral+ANTI_NEUTRAL), ROM does NOT keep the item in inventory. `equip_char`
168
+ emits a TO_CHAR + TO_ROOM message using `$p` (the object's short_descr) and
169
+ drops the item to the floor (`obj_from_char` + `obj_to_room`). The prior
170
+ Python returned a generic "the item" message, emitted no room line, and left
171
+ the item CARRIED — a state divergence a player could observe (the item stays
172
+ in inventory instead of lying on the ground).
173
+
174
+ Residual: ROM prints the "You wear $p ..." line *before* the zap (the zap
175
+ lives in equip_char, called after the wear message). Python checks alignment
176
+ up-front, so it emits only the zap line — a minor cosmetic ordering residual;
177
+ the observable state (item on the floor) now matches ROM.
178
+ """
179
+ from mud.commands.obj_manipulation import _obj_from_char
180
+
181
+ obj_name = getattr(obj, "short_descr", None) or getattr(obj, "name", None) or "it"
182
+ room = getattr(ch, "room", None)
183
+ _obj_from_char(ch, obj) # ROM obj_from_char (src/handler.c:1771)
184
+ if room is not None:
185
+ room.add_object(obj) # ROM obj_to_room (src/handler.c:1772)
186
+ # ROM src/handler.c:1770 — act("$n is zapped by $p and drops it.", TO_ROOM)
187
+ act_to_room(room, "$n is zapped by $p and drops it.", ch, arg1=obj, exclude=ch)
188
+ # ROM src/handler.c:1769 — act("You are zapped by $p and drop it.", TO_CHAR)
189
+ return f"You are zapped by {obj_name} and drop it."
190
+
191
+
144
192
  def do_wear(ch: Character, args: str) -> str:
145
193
  """
146
194
  Wear equipment (armor, clothing, jewelry).
@@ -232,9 +280,9 @@ def _wear_obj(ch: Character, obj: Object, fReplace: bool = True) -> str:
232
280
  return f"You can't remove {existing_name}."
233
281
 
234
282
  # Alignment zap (mirrors the HOLD/wear paths' _can_wear_alignment block).
235
- can_wear, error_msg = _can_wear_alignment(ch, obj)
283
+ can_wear, _ = _can_wear_alignment(ch, obj)
236
284
  if not can_wear:
237
- return error_msg or "You cannot hold that item."
285
+ return _zap_align(ch, obj) # WEAR-014: drop to room per ROM equip_char
238
286
 
239
287
  if not equipment:
240
288
  ch.equipment = {}
@@ -258,8 +306,14 @@ def _wear_obj(ch: Character, obj: Object, fReplace: bool = True) -> str:
258
306
  # then performs the two-hand-weapon check. Implemented below after the
259
307
  # generic slot-remove (search "WEAR-009 SHIELD post-check").
260
308
 
261
- # Check if item has HOLD flag - if so, use hold logic (ROM lines 1670-1677)
262
- if wear_flags & WearFlag.HOLD:
309
+ # Check if item has HOLD flag - if so, use hold logic (ROM lines 1670-1677).
310
+ # WEAR-015: ROM `wear_obj` dispatches wear flags in bit order, so every
311
+ # armor/shield slot (WEAR_FINGER..WEAR_WRIST, WEAR_SHIELD — all bits below
312
+ # HOLD's 1<<14) is checked BEFORE the HOLD branch. Only fall to HOLD when the
313
+ # object has NO earlier wear-location flag; otherwise let `_get_wear_location`
314
+ # place it in the armor slot (matching ROM's precedence). WIELD (1<<13) is
315
+ # item_type-dispatched separately above, so it's excluded from this mask.
316
+ if (wear_flags & WearFlag.HOLD) and not (wear_flags & _PRE_HOLD_WEAR_MASK):
263
317
  # Check if hold slot is occupied
264
318
  equipment = getattr(ch, "equipment", {})
265
319
  hold_loc = int(WearLocation.HOLD)
@@ -274,9 +328,9 @@ def _wear_obj(ch: Character, obj: Object, fReplace: bool = True) -> str:
274
328
  return f"You can't remove {existing_name}."
275
329
 
276
330
  # Check alignment restrictions
277
- can_hold, error_msg = _can_wear_alignment(ch, obj)
331
+ can_hold, _ = _can_wear_alignment(ch, obj)
278
332
  if not can_hold:
279
- return error_msg or "You cannot hold that item."
333
+ return _zap_align(ch, obj) # WEAR-014: drop to room per ROM equip_char
280
334
 
281
335
  # Hold the item
282
336
  if not equipment:
@@ -348,11 +402,11 @@ def _wear_obj(ch: Character, obj: Object, fReplace: bool = True) -> str:
348
402
  return "Your hands are tied up with your weapon!"
349
403
 
350
404
  # Check alignment restrictions (ROM src/handler.c:1765-1777)
351
- can_wear, error_msg = _can_wear_alignment(ch, obj)
405
+ can_wear, _ = _can_wear_alignment(ch, obj)
352
406
  if not can_wear:
353
- # In ROM, the zap happens in equip_char and item drops to room
354
- # For now, just prevent wearing with error message
355
- return error_msg or "You cannot wear that item."
407
+ # WEAR-014: ROM's zap (equip_char) drops the item to the room and emits
408
+ # a $p-substituted TO_CHAR + TO_ROOM message — not a generic in-inventory error.
409
+ return _zap_align(ch, obj)
356
410
 
357
411
  # Wear the item
358
412
  if not equipment:
@@ -423,9 +477,9 @@ def _dispatch_wield(ch: Character, obj: Object, fReplace: bool = True) -> str:
423
477
  if weight > _str_wield_max(str_stat):
424
478
  return "It is too heavy for you to wield."
425
479
 
426
- can_wield, error_msg = _can_wear_alignment(ch, obj)
480
+ can_wield, _ = _can_wear_alignment(ch, obj)
427
481
  if not can_wield:
428
- return error_msg or "You cannot wield that weapon."
482
+ return _zap_align(ch, obj) # WEAR-014: drop to room per ROM equip_char
429
483
 
430
484
  # ROM src/act_obj.c:1631-1636 — two-hand vs shield check skipped for NPCs
431
485
  # and for characters of SIZE_LARGE or greater.
@@ -435,7 +489,9 @@ def _dispatch_wield(ch: Character, obj: Object, fReplace: bool = True) -> str:
435
489
  if weapon_flags[4] & WeaponFlag.TWO_HANDS:
436
490
  shield_loc = int(WearLocation.SHIELD)
437
491
  if shield_loc in equipment and equipment[shield_loc] is not None:
438
- return "You need two hands free for that weapon!"
492
+ # WEAR-013: ROM src/act_obj.c:1635 ends this with a PERIOD, not a
493
+ # "!" (unlike the shield-branch "...weapon!" at :1606).
494
+ return "You need two hands free for that weapon."
439
495
 
440
496
  if not equipment:
441
497
  ch.equipment = {}
mud/commands/healer.py CHANGED
@@ -240,7 +240,12 @@ def do_heal(char: Character, args: str = "") -> str:
240
240
 
241
241
  arg = (args or "").strip().lower()
242
242
  if not arg:
243
- lines = [f"{_healer_name(healer)} says 'I offer the following spells:'"]
243
+ # HEALER-007: ROM src/healer.c:67 — act("$N says 'I offer the following
244
+ # spells:'", ...); act_new (src/comm.c:2379) capitalizes buf[0], so a
245
+ # lowercase-initial short_descr renders "A healer says ...". Match the
246
+ # sibling "not enough gold" branch (capitalize_act_line, INV-029/ACT-CAP).
247
+ header = capitalize_act_line(f"{_healer_name(healer)} says 'I offer the following spells:'")
248
+ lines = [header]
244
249
  lines.extend(service.display_line for service in _SERVICES)
245
250
  lines.append(" Type heal <type> to be healed.")
246
251
  return "\n".join(lines)
mud/commands/info.py CHANGED
@@ -353,8 +353,13 @@ def do_where(char: Character, args: str) -> str:
353
353
 
354
354
  found = False
355
355
 
356
- # ROM C iterates char_list for all characters (NPCs + players)
357
- for victim in character_registry:
356
+ # ROM C iterates char_list from the head and returns the FIRST match
357
+ # (src/act_info.c:2445, with `break`). ROM head-inserts new chars into
358
+ # char_list (`create_mobile`, src/db.c:2256-2257), so a head-first walk is
359
+ # newest-first. Python `character_registry` is append-ordered (oldest
360
+ # first), so we iterate it reversed to reproduce ROM's newest-first match
361
+ # when several same-named mobs share an area (FINDING-044).
362
+ for victim in reversed(character_registry):
358
363
  victim_room = getattr(victim, "room", None)
359
364
  if not victim_room:
360
365
  continue
@@ -4,7 +4,7 @@ from mud.models.character import Character
4
4
  from mud.models.constants import Direction
5
5
  from mud.utils.act import act_to_room
6
6
  from mud.world.look import dir_names, look
7
- from mud.world.vision import can_see_character, describe_character
7
+ from mud.world.vision import can_see_character, pers
8
8
 
9
9
 
10
10
  def do_scan(char: Character, args: str = "") -> str:
@@ -61,7 +61,11 @@ def do_scan(char: Character, args: str = "") -> str:
61
61
  continue
62
62
  if not can_see_character(char, p):
63
63
  continue
64
- who = describe_character(char, p)
64
+ # ROM scan_char (src/scan.c:133) uses PERS(victim, ch) — the bare
65
+ # short_descr/name, NOT show_char_to_char aura tags. describe_character
66
+ # injects (Pink/White Aura) prefixes, which ROM's scan never shows
67
+ # (FINDING-042).
68
+ who = pers(p, char)
65
69
  if depth == 0:
66
70
  lines.append(f"{who}, {distance[0]}")
67
71
  else:
mud/commands/inventory.py CHANGED
@@ -810,6 +810,18 @@ def _show_inventory_list(objects: list[Object], char: Character, show_nothing: b
810
810
  else:
811
811
  return "Nothing.\n"
812
812
 
813
+ # INVEN-001: ROM show_list_to_char formats each object via
814
+ # format_obj_to_char(obj, ch, fShort) (src/act_info.c:166), which prepends
815
+ # the object status tags ((Invis)/(Red Aura)/(Blue Aura)/(Magical)/(Glowing)/
816
+ # (Humming)) before the short_descr. That prefixed string is ALSO the
817
+ # combine/dedup key (strcmp at :180), so a glowing item and a plain identical
818
+ # item render as two separate lines instead of coalescing. Bare short_descr
819
+ # dropped the prefixes AND keyed the dedup on the wrong (prefix-blind) string.
820
+ from mud.utils.act import format_obj_to_char
821
+
822
+ def _display_name(obj: Object) -> str:
823
+ return format_obj_to_char(obj, char, f_short=True) or obj.short_descr or obj.name or "something"
824
+
813
825
  # Format objects (ROM C lines 162-225)
814
826
  if combine_enabled:
815
827
  # Combine duplicate objects (ROM C lines 170-195)
@@ -817,8 +829,8 @@ def _show_inventory_list(objects: list[Object], char: Character, show_nothing: b
817
829
  object_order: list[str] = []
818
830
 
819
831
  for obj in visible_objects:
820
- # Get object description (short description)
821
- obj_desc = obj.short_descr or obj.name or "something"
832
+ # Get object description (prefix-inclusive, per ROM dedup key)
833
+ obj_desc = _display_name(obj)
822
834
 
823
835
  # ROM C lines 176-184: Look for duplicates (case sensitive)
824
836
  if obj_desc in object_counts:
@@ -844,8 +856,7 @@ def _show_inventory_list(objects: list[Object], char: Character, show_nothing: b
844
856
  # No combining: show each object on separate line (ROM C lines 222-223)
845
857
  lines = []
846
858
  for obj in visible_objects:
847
- obj_desc = obj.short_descr or obj.name or "something"
848
- lines.append(obj_desc)
859
+ lines.append(_display_name(obj))
849
860
 
850
861
  return "\n".join(lines) + "\n"
851
862
 
@@ -912,8 +923,14 @@ def do_equipment(char: Character, args: str = "") -> str:
912
923
 
913
924
  # ROM C line 2277: if (can_see_obj (ch, obj))
914
925
  if can_see_object(char, obj):
915
- # ROM C line 2279: format_obj_to_char (obj, ch, TRUE)
916
- obj_name = obj.short_descr or obj.name or "object"
926
+ # EQUIP-002: ROM C line 2279 renders worn items via
927
+ # format_obj_to_char(obj, ch, TRUE), which prepends the object
928
+ # status tags ((Invis)/(Red Aura)/(Blue Aura)/(Magical)/(Glowing)/
929
+ # (Humming)) before the short_descr. The bare short_descr dropped
930
+ # every status prefix from `equipment` output.
931
+ from mud.utils.act import format_obj_to_char
932
+
933
+ obj_name = format_obj_to_char(obj, char, f_short=True) or obj.short_descr or obj.name or "object"
917
934
  else:
918
935
  # ROM C line 2283: send_to_char ("something.\n\r", ch);
919
936
  obj_name = "something."
@@ -265,9 +265,9 @@ def do_put(char: Character, args: str) -> str:
265
265
  messages.append(f"You put {obj_short} in {container_short}.")
266
266
  count += 1
267
267
 
268
- if count == 0:
269
- return "You have nothing to put."
270
-
268
+ # PUT-005: ROM do_put's put-all branch (src/act_obj.c:451-491) is a bare
269
+ # loop with no `found` flag and no trailing message — nothing eligible
270
+ # ROM prints nothing. The prior "You have nothing to put." was non-ROM.
271
271
  return "\n".join(messages)
272
272
 
273
273
 
mud/commands/session.py CHANGED
@@ -149,15 +149,18 @@ def do_score(ch: Character, args: str) -> str:
149
149
  lines.append(f"You have {practice} practices and {train} training sessions.")
150
150
 
151
151
  # Carrying - ROM src/act_info.c:1514-1518 (immediately after practices)
152
- carry_weight = getattr(ch, "carry_weight", 0)
153
152
  carry_number = getattr(ch, "carry_number", 0)
154
- from mud.world.movement import can_carry_n, can_carry_w
153
+ from mud.world.movement import can_carry_n, can_carry_w, get_carry_weight
155
154
 
155
+ # SCORE-002: ROM src/act_info.c:1517 prints `get_carry_weight (ch) / 10`,
156
+ # which adds coin weight (`silver/10 + gold*2/5`, src/merc.h:2118) to the
157
+ # raw item weight. Using bare `ch.carry_weight` dropped coin burden from the
158
+ # score sheet. get_carry_weight is non-negative, so `// 10` == C truncation.
156
159
  max_carry_number = can_carry_n(ch)
157
160
  max_carry_weight = can_carry_w(ch) // 10 # ROM divides by 10 for display
158
161
  lines.append(
159
162
  f"You are carrying {carry_number}/{max_carry_number} items "
160
- f"with weight {carry_weight // 10}/{max_carry_weight} pounds."
163
+ f"with weight {get_carry_weight(ch) // 10}/{max_carry_weight} pounds."
161
164
  )
162
165
 
163
166
  # Stats - ROM src/act_info.c:1520-1531 (perm(current) per stat).
@@ -339,15 +342,21 @@ def do_recall(ch: Character, args: str) -> str:
339
342
  ROM Reference: src/act_move.c lines 1563-1628 (do_recall)
340
343
  """
341
344
  from mud.combat.engine import stop_fighting
342
- from mud.models.constants import ROOM_VNUM_TEMPLE, AffectFlag, RoomFlag
345
+
346
+ # RECALL-003: NPCs can't recall unless they carry ACT_PET (ROM
347
+ # src/act_move.c:1569-1573 — `IS_NPC(ch) && !IS_SET(ch->act, ACT_PET)`), and
348
+ # ROM `send_to_char("Only players can recall.")` then returns — it does NOT
349
+ # return silently, and it gates on the ACT_PET *flag*, not on `master` (a
350
+ # charmed non-pet mob is still blocked). The prior code returned "" and keyed
351
+ # on `master`, so a charmed non-pet mob would wrongly recall and no message
352
+ # was shown when an NPC path (e.g. a switched immortal) hit this.
353
+ from mud.models.constants import ROOM_VNUM_TEMPLE, ActFlag, AffectFlag, RoomFlag
343
354
  from mud.registry import room_registry
344
355
  from mud.utils.rng_mm import number_percent
345
356
 
346
- # NPCs can't recall unless they're pets (ROM C lines 1569-1573).
347
- # ROM uses IS_NPC + IS_AFFECTED(AFF_CHARM); the QuickMUD analogue is an NPC
348
- # that has a master (i.e. it's been charmed/owned).
349
- if ch.is_npc and getattr(ch, "master", None) is None:
350
- return "" # ROM returns silently (src/act_move.c:1569-1573)
357
+ act_flags = int(getattr(ch, "act", 0) or 0)
358
+ if ch.is_npc and not (act_flags & int(ActFlag.PET)):
359
+ return "Only players can recall."
351
360
 
352
361
  # Message to room FIRST (ROM C line 1575)
353
362
  if ch.room:
mud/commands/socials.py CHANGED
@@ -57,8 +57,11 @@ def perform_social(char: Character, name: str, arg: str) -> str:
57
57
  if position == Position.STUNNED:
58
58
  return "You are too stunned to do that."
59
59
  # mirroring ROM src/interp.c:618-626 — POS_SLEEPING blocks every social
60
- # except "snore" (the canonical Furey exception).
61
- if position == Position.SLEEPING and name.lower() != "snore":
60
+ # except "snore" (the canonical Furey exception). SOCIAL-001: ROM compares
61
+ # the RESOLVED social's name (`social_table[cmd].name`, interp.c:623), NOT
62
+ # the typed argument, so a prefix like "snor" that resolves to snore is also
63
+ # allowed while asleep. `social` was already resolved via find_social above.
64
+ if position == Position.SLEEPING and social.name.lower() != "snore":
62
65
  return "In your dreams, or what?"
63
66
  victim = None
64
67
  if arg:
mud/skills/handlers.py CHANGED
@@ -1328,53 +1328,53 @@ def bash(
1328
1328
  raise ValueError("bash requires both caster and target")
1329
1329
 
1330
1330
  bash_type = int(DamageType.BASH)
1331
- caster_name = _character_name(caster)
1332
- target_name = _character_name(target)
1333
1331
  room = getattr(caster, "room", None)
1334
1332
 
1333
+ # BASH-001: ROM do_bash (src/fight.c:2460-2482) calls damage(..., FALSE) on
1334
+ # BOTH branches — show=FALSE suppresses the dam_message, so the three {5..{x
1335
+ # flavor act() lines are the only output. The attacker's TO_CHAR flavor line
1336
+ # is the command return; TO_VICT/TO_NOTVICT are pushed. apply_damage(show=False)
1337
+ # pushes nothing, so returning the flavor line is single-delivery (no double).
1335
1338
  if not success:
1336
- # mirroring ROM src/fight.c:2478-2481 — failure broadcasts
1339
+ # ROM src/fight.c:2477-2481 — damage() first, then TO_CHAR/TO_NOTVICT/TO_VICT.
1340
+ apply_damage(caster, target, 0, bash_type, dt="bash", show=False)
1337
1341
  if room is not None:
1338
- caster_his = _possessive_pronoun(caster)
1339
- caster_him = _objective_pronoun(caster)
1340
- _notvict_broadcast(
1341
- room,
1342
- caster,
1343
- target,
1344
- f"{caster_name} falls flat on {caster_his} face.",
1345
- )
1342
+ act_to_room(room, "{5$n falls flat on $s face.{x", caster, exclude=caster)
1346
1343
  _to_vict_send(
1347
1344
  target,
1348
- f"You evade {caster_name}'s bash, causing {caster_him} to fall flat on {caster_his} face.",
1345
+ act_format(
1346
+ "{5You evade $n's bash, causing $m to fall flat on $s face.{x",
1347
+ recipient=target,
1348
+ actor=caster,
1349
+ arg2=target,
1350
+ ),
1349
1351
  )
1350
- return apply_damage(caster, target, 0, bash_type, dt="bash")
1352
+ return act_format("{5You fall flat on your face!{x", recipient=caster, actor=caster, arg2=target)
1351
1353
 
1352
1354
  chance = int(chance or 0)
1353
1355
  size = max(0, int(getattr(caster, "size", 0) or 0))
1354
1356
  upper = 2 + 2 * size + c_div(chance, 20)
1355
1357
  damage = rng_mm.number_range(2, max(2, upper))
1356
1358
 
1357
- # mirroring ROM src/fight.c:2459-2465 — success broadcasts
1359
+ # ROM src/fight.c:2460-2465 — success broadcasts ({5..{x, PERS-rendered).
1358
1360
  if room is not None:
1359
1361
  _to_vict_send(
1360
1362
  target,
1361
- f"{caster_name} sends you sprawling with a powerful bash!",
1362
- )
1363
- _notvict_broadcast(
1364
- room,
1365
- caster,
1366
- target,
1367
- f"{caster_name} sends {target_name} sprawling with a powerful bash.",
1363
+ act_format("{5$n sends you sprawling with a powerful bash!{x", recipient=target, actor=caster, arg2=target),
1368
1364
  )
1365
+ act_to_room(room, "{5$n sends $N sprawling with a powerful bash.{x", caster, arg2=target, exclude=target)
1369
1366
 
1370
- # DAZE_STATE in ROM applies 3 * PULSE_VIOLENCE to the victim.
1367
+ # DAZE_STATE(victim, 3 * PULSE_VIOLENCE); ROM :2470-2472 position=RESTING then
1368
+ # damage(show=FALSE) (dam_message suppressed).
1371
1369
  from mud.config import get_pulse_violence
1372
1370
 
1373
1371
  victim_daze = 3 * get_pulse_violence()
1374
1372
  target.daze = max(int(getattr(target, "daze", 0) or 0), victim_daze)
1375
- result = apply_damage(caster, target, damage, bash_type, dt="bash")
1373
+ apply_damage(caster, target, damage, bash_type, dt="bash", show=False)
1374
+ # ROM sets POS_RESTING before damage() (:2470); in Python apply_damage's
1375
+ # set_fighting resets position, so RESTING is applied AFTER to make it stick.
1376
1376
  target.position = Position.RESTING
1377
- return result
1377
+ return act_format("{5You slam into $N, and send $M flying!{x", recipient=caster, actor=caster, arg2=target)
1378
1378
 
1379
1379
 
1380
1380
  def berserk(
@@ -3559,16 +3559,114 @@ def dispel_magic(caster: Character, target: Character | None = None) -> bool:
3559
3559
  _send_to_char(caster, "You failed.")
3560
3560
  return False
3561
3561
 
3562
- effects = getattr(target, "spell_effects", {})
3563
- if not isinstance(effects, dict) or not effects:
3564
- return False
3562
+ # MAGIC-050: ROM spell_dispel_magic (src/magic.c:2089-2247) walks a FIXED
3563
+ # hardcoded spell list in order — not the spell_effects dict calling
3564
+ # check_dispel per spell, with per-effect act(TO_ROOM) wear-off lines for
3565
+ # certain spells, an AFF_SANCTUARY-bit fallback, and a final Ok./Spell failed.
3566
+ # to the caster. The dict-iteration port lost the order, the room messages,
3567
+ # the sanctuary-bit strip, and the result message. This mirrors the sibling
3568
+ # `cancellation` walk (same list) minus its NPC gate and level+2.
3569
+ found = False
3570
+ room = getattr(target, "room", None)
3571
+
3572
+ def _dispel(effect_name: str) -> bool:
3573
+ return check_dispel(level, target, effect_name)
3574
+
3575
+ if _dispel("armor"):
3576
+ found = True
3577
+ if _dispel("bless"):
3578
+ found = True
3579
+ if _dispel("blindness"):
3580
+ found = True
3581
+ act_to_room(room, "$n is no longer blinded.", target, exclude=target)
3582
+ if _dispel("calm"):
3583
+ found = True
3584
+ act_to_room(room, "$n no longer looks so peaceful...", target, exclude=target)
3585
+ if _dispel("change_sex"):
3586
+ found = True
3587
+ act_to_room(room, "$n looks more like $mself again.", target, exclude=target)
3588
+ if _dispel("charm_person"):
3589
+ found = True
3590
+ act_to_room(room, "$n regains $s free will.", target, exclude=target)
3591
+ if _dispel("chill_touch"):
3592
+ found = True
3593
+ act_to_room(room, "$n looks warmer.", target, exclude=target)
3594
+ if _dispel("curse"):
3595
+ found = True
3596
+ if _dispel("detect_evil"):
3597
+ found = True
3598
+ if _dispel("detect_good"):
3599
+ found = True
3600
+ if _dispel("detect_hidden"):
3601
+ found = True
3602
+ if _dispel("detect_invis"):
3603
+ found = True
3604
+ if _dispel("detect_magic"):
3605
+ found = True
3606
+ if _dispel("faerie_fire"):
3607
+ act_to_room(room, "$n's outline fades.", target, exclude=target)
3608
+ found = True
3609
+ if _dispel("fly"):
3610
+ act_to_room(room, "$n falls to the ground!", target, exclude=target)
3611
+ found = True
3612
+ if _dispel("frenzy"):
3613
+ act_to_room(room, "$n no longer looks so wild.", target, exclude=target)
3614
+ found = True
3615
+ if _dispel("giant_strength"):
3616
+ act_to_room(room, "$n no longer looks so mighty.", target, exclude=target)
3617
+ found = True
3618
+ if _dispel("haste"):
3619
+ act_to_room(room, "$n is no longer moving so quickly.", target, exclude=target)
3620
+ found = True
3621
+ if _dispel("infravision"):
3622
+ found = True
3623
+ if _dispel("invis"):
3624
+ act_to_room(room, "$n fades into existance.", target, exclude=target)
3625
+ found = True
3626
+ if _dispel("mass_invis"):
3627
+ act_to_room(room, "$n fades into existance.", target, exclude=target)
3628
+ found = True
3629
+ if _dispel("pass_door"):
3630
+ found = True
3631
+ if _dispel("protection_evil"):
3632
+ found = True
3633
+ if _dispel("protection_good"):
3634
+ found = True
3635
+ if _dispel("sanctuary"):
3636
+ act_to_room(room, "The white aura around $n's body vanishes.", target, exclude=target)
3637
+ found = True
3638
+ # ROM src/magic.c:2209-2217 — an AFF_SANCTUARY *bit* not backed by a
3639
+ # "sanctuary" spell effect (innate/item) is stripped on a failed saves_dispel.
3640
+ if (
3641
+ target.has_affect(AffectFlag.SANCTUARY)
3642
+ and not saves_dispel(level, int(getattr(target, "level", 0) or 0), -1)
3643
+ and "sanctuary" not in getattr(target, "spell_effects", {})
3644
+ ):
3645
+ target.remove_affect(AffectFlag.SANCTUARY)
3646
+ act_to_room(room, "The white aura around $n's body vanishes.", target, exclude=target)
3647
+ found = True
3648
+ if _dispel("shield"):
3649
+ act_to_room(room, "The shield protecting $n vanishes.", target, exclude=target)
3650
+ found = True
3651
+ if _dispel("sleep"):
3652
+ found = True
3653
+ if _dispel("slow"):
3654
+ act_to_room(room, "$n is no longer moving so slowly.", target, exclude=target)
3655
+ found = True
3656
+ if _dispel("stone_skin"):
3657
+ act_to_room(room, "$n's skin regains its normal texture.", target, exclude=target)
3658
+ found = True
3659
+ if _dispel("weaken"):
3660
+ act_to_room(room, "$n looks stronger.", target, exclude=target)
3661
+ found = True
3565
3662
 
3566
- success = False
3567
- for effect_name in list(effects.keys()):
3568
- if check_dispel(level, target, effect_name):
3569
- success = True
3663
+ # ROM src/magic.c:2249-2251 — result message to the caster.
3664
+ if found:
3665
+ _send_to_char(caster, "Ok.")
3666
+ else:
3667
+ _send_to_char(caster, "Spell failed.")
3570
3668
 
3571
- return success
3669
+ return found
3572
3670
 
3573
3671
 
3574
3672
  def earthquake(caster: Character, target=None) -> bool: # noqa: ARG001 - parity signature
@@ -5218,10 +5316,13 @@ def heat_metal(
5218
5316
  room = getattr(target, "room", None)
5219
5317
  iter_carrying = getattr(target, "iter_carrying", None)
5220
5318
  if callable(iter_carrying):
5319
+ # Both Character (re-merged inventory+equipment by _carry_seq) and
5320
+ # MobInstance (single head-inserted inventory list — MAGIC-046) expose
5321
+ # iter_carrying in ROM victim->carrying LIFO order.
5221
5322
  all_items = list(iter_carrying())
5222
5323
  else:
5223
- # MobInstance has no _carry_seq acquisition tracking yet; fall back to the
5224
- # inventory (already head-inserted LIFO) + equipment combination.
5324
+ # Last-resort fallback for any other carrier type: inventory (head-inserted
5325
+ # LIFO) + equipment combination.
5225
5326
  inventory = list(getattr(target, "inventory", []) or [])
5226
5327
  equipment = dict(getattr(target, "equipment", {}) or {})
5227
5328
  all_items = inventory + list(equipment.values())
mud/spawning/templates.py CHANGED
@@ -500,6 +500,20 @@ class MobInstance:
500
500
  self.inventory.insert(0, obj)
501
501
  obj.carried_by = self
502
502
 
503
+ def iter_carrying(self) -> list[Object]:
504
+ """Return this mob's items in ROM ``victim->carrying`` (LIFO) order.
505
+
506
+ MAGIC-046: unlike ``Character`` (which splits carried ``inventory`` from
507
+ worn ``equipment`` and must re-merge by ``_carry_seq``), a mob keeps
508
+ worn and carried items in the single ``inventory`` list — equip only
509
+ flips ``wear_loc`` (FINDING-025), and every acquisition path
510
+ (``add_to_inventory``/``equip``, reset G/E) head-inserts, so the list is
511
+ already newest → oldest exactly like ROM's ``ch->carrying``. Mechanics
512
+ mirroring ROM's carrying walk (``heat_metal``) iterate this instead of
513
+ the generic ``inventory + equipment.values()`` fallback.
514
+ """
515
+ return list(self.inventory)
516
+
503
517
  def remove_object(self, obj: Object) -> None:
504
518
  # ROM src/handler.c obj_from_char: unequip first if worn, then strip
505
519
  # from carried list. Mirror that here so disarm/remove paths actually
mud/world/look.py CHANGED
@@ -2,8 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from mud.math.c_compat import c_div
4
4
  from mud.models.character import Character
5
- from mud.models.constants import AffectFlag, Direction, Position
6
- from mud.world.vision import can_see_character, describe_character
5
+ from mud.models.constants import LEVEL_HERO, AffectFlag, CommFlag, Direction, PlayerFlag, Position
6
+ from mud.world.vision import can_see_character, pers
7
7
 
8
8
  # mirroring ROM src/act_info.c show_char_to_char_0 — buf[0] = UPPER(buf[0]) and
9
9
  # position suffixes appended when a mob is not at its start/default position.
@@ -21,26 +21,69 @@ _POSITION_SUFFIX: dict[Position, str] = {
21
21
  }
22
22
 
23
23
 
24
+ def _char_tags(observer: Character, victim) -> str:
25
+ """Build ROM show_char_to_char_0's status-tag prefix (src/act_info.c:253-276).
26
+
27
+ The full fixed order is [AFK] (Invis) (Wizi) (Hide) (Charmed) (Translucent)
28
+ (Pink Aura) (Red Aura) (Golden Aura) (White Aura) (KILLER) (THIEF). Red/Golden
29
+ auras depend on the *observer* carrying detect-evil/good and the victim's
30
+ alignment; KILLER/THIEF are PC-only PLR flags. Tags live here (the room
31
+ listing), not in PERS/``describe_character`` — see ``pers`` docstring.
32
+ """
33
+ tags: list[str] = []
34
+ has_affect = getattr(victim, "has_affect", None)
35
+
36
+ if int(getattr(victim, "comm", 0) or 0) & int(CommFlag.AFK):
37
+ tags.append("[AFK]")
38
+ if callable(has_affect):
39
+ if victim.has_affect(AffectFlag.INVISIBLE):
40
+ tags.append("(Invis)")
41
+ if int(getattr(victim, "invis_level", 0) or 0) >= LEVEL_HERO:
42
+ tags.append("(Wizi)")
43
+ if callable(has_affect):
44
+ if victim.has_affect(AffectFlag.HIDE):
45
+ tags.append("(Hide)")
46
+ if victim.has_affect(AffectFlag.CHARM):
47
+ tags.append("(Charmed)")
48
+ if victim.has_affect(AffectFlag.PASS_DOOR):
49
+ tags.append("(Translucent)")
50
+ if victim.has_affect(AffectFlag.FAERIE_FIRE):
51
+ tags.append("(Pink Aura)")
52
+
53
+ victim_align = int(getattr(victim, "alignment", 0) or 0)
54
+ obs_has_affect = getattr(observer, "has_affect", None)
55
+ # ROM IS_EVIL: alignment <= -350; IS_GOOD: alignment >= 350.
56
+ if victim_align <= -350 and callable(obs_has_affect) and observer.has_affect(AffectFlag.DETECT_EVIL):
57
+ tags.append("(Red Aura)")
58
+ if victim_align >= 350 and callable(obs_has_affect) and observer.has_affect(AffectFlag.DETECT_GOOD):
59
+ tags.append("(Golden Aura)")
60
+
61
+ if callable(has_affect) and victim.has_affect(AffectFlag.SANCTUARY):
62
+ tags.append("(White Aura)")
63
+
64
+ if not getattr(victim, "is_npc", False):
65
+ victim_act = int(getattr(victim, "act", 0) or 0)
66
+ if victim_act & int(PlayerFlag.KILLER):
67
+ tags.append("(KILLER)")
68
+ if victim_act & int(PlayerFlag.THIEF):
69
+ tags.append("(THIEF)")
70
+
71
+ return (" ".join(tags) + " ") if tags else ""
72
+
73
+
24
74
  def _room_occupant_line(observer: Character, victim) -> str:
25
75
  """Render a room-list occupant — ROM src/act_info.c show_char_to_char_0.
26
76
 
27
77
  An NPC whose position equals its start/default position and which has a
28
- non-empty long_descr is listed by that long_descr (with affect auras
78
+ non-empty long_descr is listed by that long_descr (with the status tags
29
79
  prefixed), e.g. "Hassan is here, waiting to dispense some justice."
30
80
  Otherwise show PERS(victim) + a position suffix ("is resting here.", etc.).
31
81
  """
32
- # LOOK-010: aura tags. ROM show_char_to_char_0 (src/act_info.c:266,272) prints
33
- # (Pink Aura) [FAERIE_FIRE] before (White Aura) [SANCTUARY]. This `prefix` is
34
- # used ONLY by the NPC long_descr branch below; the PERS branch gets its auras
35
- # from describe_character() (which also prepends them) prepending here too
36
- # double-rendered them.
37
- prefixes = []
38
- if hasattr(victim, "has_affect"):
39
- if victim.has_affect(AffectFlag.FAERIE_FIRE):
40
- prefixes.append("(Pink Aura)")
41
- if victim.has_affect(AffectFlag.SANCTUARY):
42
- prefixes.append("(White Aura)")
43
- prefix = (" ".join(prefixes) + " ") if prefixes else ""
82
+ # LOOK char-tags: ROM builds the full status-tag prefix (all 12) then appends
83
+ # either long_descr or PERS. Use pers() for the base name (pure PERS, no aura
84
+ # injection) so the tags render once and in ROM order describe_character is
85
+ # left for its combat/other callers, which a direct unit test locks.
86
+ prefix = _char_tags(observer, victim)
44
87
 
45
88
  long_descr = getattr(victim, "long_descr", None)
46
89
  # ROM uses start_pos for the long_descr gate; Python stores default_pos (same
@@ -49,8 +92,21 @@ def _room_occupant_line(observer: Character, victim) -> str:
49
92
  if getattr(victim, "is_npc", False) and long_descr and getattr(victim, "position", None) == ref_pos:
50
93
  return prefix + str(long_descr).rstrip("\r\n")
51
94
 
52
- pers = describe_character(observer, victim)
95
+ base = prefix + pers(victim, observer)
53
96
  position = getattr(victim, "position", None)
97
+ # LOOK-017: ROM src/act_info.c:285-288 appends the victim's pcdata->title
98
+ # after PERS for a standing PC — gated on the OBSERVER's COMM_BRIEF and
99
+ # furniture (ch->comm / ch->on), a deliberate ROM quirk.
100
+ if (
101
+ not getattr(victim, "is_npc", False)
102
+ and not (int(getattr(observer, "comm", 0) or 0) & int(CommFlag.BRIEF))
103
+ and position == Position.STANDING
104
+ and getattr(observer, "on", None) is None
105
+ ):
106
+ _pcdata = getattr(victim, "pcdata", None)
107
+ _title = getattr(_pcdata, "title", None) if _pcdata is not None else None
108
+ if _title:
109
+ base += _title
54
110
  fighting = getattr(victim, "fighting", None)
55
111
  if position == Position.FIGHTING:
56
112
  # mirroring ROM src/act_info.c:404-417 show_char_to_char_0 POS_FIGHTING
@@ -59,14 +115,17 @@ def _room_occupant_line(observer: Character, victim) -> str:
59
115
  elif fighting is observer:
60
116
  fight_str = "YOU!"
61
117
  elif getattr(victim, "room", None) is getattr(fighting, "room", object()):
62
- fight_str = describe_character(observer, fighting) + "."
118
+ # ROM src/act_info.c:412 uses PERS(victim->fighting, ch) the bare
119
+ # name, NOT the show_char_to_char aura block. describe_character injects
120
+ # (Pink/White Aura), which ROM never shows here (FINDING-043, same class
121
+ # as the scan FINDING-042 aura/PERS leak).
122
+ fight_str = pers(fighting, observer) + "."
63
123
  else:
64
124
  fight_str = "someone who left??"
65
- # pers (describe_character) already carries the aura prefix do not re-add.
66
- line = pers + " is here, fighting " + fight_str
125
+ line = base + " is here, fighting " + fight_str
67
126
  else:
68
127
  suffix = _POSITION_SUFFIX.get(position, "") if position is not None else ""
69
- line = pers + suffix
128
+ line = base + suffix
70
129
  # mirroring ROM src/act_info.c:421 buf[0] = UPPER(buf[0])
71
130
  return line[0].upper() + line[1:] if line else line
72
131
 
@@ -307,20 +366,39 @@ def _look_char(char: Character, victim: Character) -> str:
307
366
  condition = f"{short} is in awful condition."
308
367
  else:
309
368
  condition = f"{short} is bleeding to death."
369
+ # ROM src/act_info.c:480 — `buf[0] = UPPER(buf[0])` capitalizes the first
370
+ # char of the health line, so a mob's lowercase short_descr ("the beastly
371
+ # fido ...") renders "The beastly fido ..." (LOOK-014).
372
+ condition = condition[0].upper() + condition[1:] if condition else condition
310
373
  lines.append(condition)
311
374
 
312
- # Show equipment if visible
313
- equipment = _show_equipment(victim)
375
+ # Show equipment if visible — ROM src/act_info.c:483-499 (LOOK-016).
376
+ equipment = _show_equipment(victim, char)
314
377
  if equipment:
315
- lines.append(f"\n{short} is using:")
378
+ # ROM act("$N is using:", ...) capitalizes the rendered first char.
379
+ header = f"{short} is using:"
380
+ header = header[0].upper() + header[1:]
381
+ lines.append(f"\n{header}")
316
382
  lines.append(equipment)
317
383
 
318
384
  return "\n".join(lines)
319
385
 
320
386
 
321
- def _show_equipment(char: Character) -> str:
322
- """Show equipped items - ROM show_char_to_char_1"""
323
- from mud.models.constants import WearLocation
387
+ def _show_equipment(victim: Character, observer: Character) -> str:
388
+ """Show *victim*'s equipped items to *observer* — ROM show_char_to_char_1.
389
+
390
+ LOOK-016: mirrors ROM ``src/act_info.c:483-499`` — loop wear slots in
391
+ ascending order, gate each on ``can_see_obj(observer, obj)``, and render
392
+ ``where_name[iWear] + format_obj_to_char(obj, observer, TRUE)`` (no indent,
393
+ with aura/status tags). The prior code read a **phantom** ``equipped``
394
+ attribute (via ``getattr``) where the real one is ``char.equipment``
395
+ (int-keyed by WearLocation), so the whole block was dead — the equipment-key
396
+ convention class via a wrong attribute NAME, invisible to the string-key
397
+ grep-guard.
398
+ """
399
+ from mud.models.constants import WearLocation # noqa: F401 (enum keys hash-equal to int)
400
+ from mud.utils.act import format_obj_to_char
401
+ from mud.world.vision import can_see_object
324
402
 
325
403
  wear_names = {
326
404
  WearLocation.LIGHT: "<used as light> ",
@@ -345,13 +423,15 @@ def _show_equipment(char: Character) -> str:
345
423
  }
346
424
 
347
425
  lines = []
348
- equipped = getattr(char, "equipped", {})
349
- if isinstance(equipped, dict):
350
- for loc, obj in equipped.items():
351
- if obj:
352
- loc_name = wear_names.get(loc, "<unknown> ")
353
- obj_name = getattr(obj, "short_descr", None) or getattr(obj, "name", "something")
354
- lines.append(f" {loc_name}{obj_name}")
426
+ equipment = getattr(victim, "equipment", {})
427
+ if isinstance(equipment, dict):
428
+ # ROM iterates iWear 0..MAX_WEAR ascending; equipment is int-keyed.
429
+ for slot in sorted(equipment.keys()):
430
+ obj = equipment[slot]
431
+ if obj is None or not can_see_object(observer, obj):
432
+ continue
433
+ loc_name = wear_names.get(slot, "<unknown> ")
434
+ lines.append(f"{loc_name}{format_obj_to_char(obj, observer, True)}")
355
435
 
356
436
  return "\n".join(lines)
357
437
 
@@ -417,10 +497,15 @@ def _look_in(char: Character, args: str) -> str:
417
497
  if value[1] <= 0:
418
498
  return "It is empty."
419
499
  if value[0] > 0:
420
- percent = value[1] * 100 // value[0]
421
- if percent < 25:
500
+ # LOOK-015: mirror ROM's exact integer comparisons
501
+ # (src/act_info.c:1141-1145) — `value[1] < value[0]/4` and
502
+ # `value[1] < 3*value[0]/4`. A rewritten `value[1]*100//value[0]`
503
+ # percentage truncates at a different point and mislabels boundary
504
+ # amounts (e.g. value=(10,2): ROM "about half-", percent form
505
+ # "less than half-"). value[0]/value[1] are non-negative, so // == C /.
506
+ if value[1] < value[0] // 4:
422
507
  amount = "less than half-"
423
- elif percent < 75:
508
+ elif value[1] < 3 * value[0] // 4:
424
509
  amount = "about half-"
425
510
  else:
426
511
  amount = "more than half-"
@@ -477,9 +562,11 @@ def _look_direction(char: Character, room, direction: int) -> str:
477
562
  keyword = getattr(exit_obj, "keyword", None)
478
563
  exit_info = getattr(exit_obj, "exit_info", 0)
479
564
 
480
- # EX_CLOSED = 1, EX_ISDOOR = 2
481
- EX_ISDOOR = 2
482
- EX_CLOSED = 1
565
+ # ROM merc.h: EX_ISDOOR = (A) = 1, EX_CLOSED = (B) = 2. Never hardcode these
566
+ # (AGENTS.md flag-values rule) — the values below were previously swapped,
567
+ # so every keyword'd door (which always has the ISDOOR bit) rendered as
568
+ # "closed" even when open (LOOK-012).
569
+ from mud.models.constants import EX_CLOSED, EX_ISDOOR
483
570
 
484
571
  if keyword and keyword.strip():
485
572
  if exit_info & EX_CLOSED:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rom24-quickmud-python
3
- Version: 2.14.264
3
+ Version: 2.14.298
4
4
  Summary: A modern Python port of the ROM 2.4b6 MUD engine with full telnet server and JSON world loading
5
5
  Author-email: Mark Jedrzejczyk <mark.jedrzejczyk@gmail.com>
6
6
  Maintainer-email: Mark Jedrzejczyk <mark.jedrzejczyk@gmail.com>
@@ -59,7 +59,7 @@ Dynamic: license-file
59
59
 
60
60
  # QuickMUD - A Modern ROM 2.4 Python Port
61
61
 
62
- [![Version](https://img.shields.io/badge/version-2.14.264-blue.svg)](https://github.com/Nostoi/rom24-quickmud-python)
62
+ [![Version](https://img.shields.io/badge/version-2.14.268-blue.svg)](https://github.com/Nostoi/rom24-quickmud-python)
63
63
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
64
64
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
65
65
  [![Tests](https://img.shields.io/badge/tests-6127%20passing-brightgreen.svg)](https://github.com/Nostoi/rom24-quickmud-python)
@@ -228,7 +228,7 @@ python -m mud # Start development server
228
228
  **Stage: parity beta** — feature-complete and playable; parity fidelity is being
229
229
  systematically hardened toward ROM-exact behavioral equivalence.
230
230
 
231
- - **Version**: 2.14.264
231
+ - **Version**: 2.14.268
232
232
  - **Playability**: ✅ All 255 ROM commands implemented. Combat, spells, skills,
233
233
  movement, shops, mob programs, OLC building, and admin tools work and pass their
234
234
  tests. You can run a server and play today.
@@ -19,7 +19,7 @@ mud/account/__init__.py,sha256=wwII0DQSMn6E8qGwukIw8is-7JEW7z-UDd6qdZDpSYU,1856
19
19
  mud/account/account_manager.py,sha256=Ud1-nE55wfsTYokZ3i7dSNle9Hc7I7D0jllUOKMSSic,15449
20
20
  mud/account/account_service.py,sha256=OmJ2CogFMZz4O7RI-THajyA5Pu5vn5Q8GCfcBXQpAh0,40597
21
21
  mud/admin_logging/__init__.py,sha256=3W8Z1S7Dg-AtDTKPQIzuqL4_OojeyuAOryGH6BKozcg,40
22
- mud/admin_logging/admin.py,sha256=9ak3Edg62E429NMCydoaFU6xJK1DniGX4fDbKtbHA2Y,5257
22
+ mud/admin_logging/admin.py,sha256=Gn99H4eORIt2KFt1zF7L4yEI0x5Cw1EYc7xoq4uo4NA,5280
23
23
  mud/admin_logging/agent_trace.py,sha256=j1rtrINBp2RIB9BRGXK0awHTH8_nvSu4Co18zmdOU2w,359
24
24
  mud/affects/engine.py,sha256=JtLaYZUyfm0hPJBmI-7-YcPOtcciGxsJGcz6d5utyrk,6690
25
25
  mud/affects/saves.py,sha256=oiHYUagVbF3MLjYqLMAnUui_qYGr1zoUNsqDAhI2xKQ,5246
@@ -34,7 +34,7 @@ mud/characters/follow.py,sha256=8CBBB6hoimoqlKmWTkfVRekTXAJgICtvfqWqhqqLFsw,4438
34
34
  mud/combat/__init__.py,sha256=YUphU7bKkxT3Qw0Gj2TcMmDoDyCYyRTQbHPCqKLjugE,215
35
35
  mud/combat/assist.py,sha256=G4mQacit5Otcm7Ou1g8lgxZJz-B5QFsGcD5Gi3W0kmI,8775
36
36
  mud/combat/death.py,sha256=S6KmG5nfPDHSsyPTK_SKqs3C7APyJ2eGF0LAjqj6HJE,25766
37
- mud/combat/engine.py,sha256=FuyBjUuZGhb78hGt7lsrMZNGp6QHydmU197cO-6FsXw,81498
37
+ mud/combat/engine.py,sha256=3Qy7knjwakWCmmVKRpdFOZotaqhW37iOoACU86kxwn0,82545
38
38
  mud/combat/kill_table.py,sha256=oR77me5GJLGXi3q5ZnQAdp3S5lsfJNhiCmaXOMQrNgA,1040
39
39
  mud/combat/messages.py,sha256=c4kBvZFk2BneiJickTF3nRl84tq-InBJcLgKGzVTXMA,8708
40
40
  mud/combat/safety.py,sha256=aRcywM5vSU3Ct2xzHW-Eeiwr71DemeU1g0oSTqCYB4U,9049
@@ -46,20 +46,20 @@ mud/commands/alias_cmds.py,sha256=SRG4hdX4HbLqQ9fbnKbPbe8AjPH4klLEdOFjeR4Ppxo,39
46
46
  mud/commands/auto_settings.py,sha256=Ui99uWb7-MZ8JDvxSih-97tdLGrIlcCBkk4hjUct-t0,14593
47
47
  mud/commands/build.py,sha256=RuirbGApM3Wm5xwlarU7VfqpF_JDef0_pSQYofzI5YU,152534
48
48
  mud/commands/channels.py,sha256=EzAIgu4Hkp1unAdpmKb7TQ1DtuYUTEl_71SO4YnBuMM,1552
49
- mud/commands/character.py,sha256=0Gi7NoRVgz3lWFSu2IS9AXm-hCb32pVxbYRlaKqIt3o,6751
50
- mud/commands/combat.py,sha256=CekrjAQBMqFpNvCDJDty8oJFrwsta5d8O1fpfWmX0g0,60182
49
+ mud/commands/character.py,sha256=rK4D0TrbU8bZyv0vx2CA5HdANTWmBV--C1JXd-TRbpk,6904
50
+ mud/commands/combat.py,sha256=hrETbdT_lUIoPmMBKenY1QWr8DEQ6ZHLaNmZcboy1zc,61633
51
51
  mud/commands/communication.py,sha256=pFNih4ZePV3ZKrstm9WMU_ooMz00g_MOFzOHk91Qu-o,34963
52
- mud/commands/compare.py,sha256=pVYrZvB5WF_IEE8AWapOFd6JMdxPgrQPkEZj2jvWkfU,4883
52
+ mud/commands/compare.py,sha256=L-vSxp33rLEZe7_HOj901aX2g_Gv3FgQblsXgIZ4fcA,5124
53
53
  mud/commands/consider.py,sha256=86stGtNOcdWI0I6sEtcDnB3R7qv6IKOGoUjbDnHuQ_c,3312
54
54
  mud/commands/consumption.py,sha256=1t2lvDNmWzIfTa61KJruhxOJLtBuTjsV_Tcu2Byi-yU,15364
55
55
  mud/commands/decorators.py,sha256=n_ezcovFkycAZOVxg0UNtMZLtiSlF0Yj8yqYt4YVY1U,345
56
56
  mud/commands/dispatcher.py,sha256=sEOpkWiFGivFobZpg7GMGHlD_fEa9D34cFDf8eW3l_Q,52889
57
- mud/commands/doors.py,sha256=Erukptb5p3ux0iQiOO0ofEo3Q4HI1JDgI0CLrrW7cas,26870
58
- mud/commands/equipment.py,sha256=xKZiGZoWh-KqNWJZGKlgSI9zdv7ZmJc28zaglNjfqn8,26613
57
+ mud/commands/doors.py,sha256=SSjYZ7J57FnxTzjT0Z1wbQa62ek1Nh_efgPgdFhGBgI,27299
58
+ mud/commands/equipment.py,sha256=bJ4lhUz4K2VDxh7cD_CtjFK1HN0OMMJuCciMWMdsFew,29550
59
59
  mud/commands/feedback.py,sha256=MJtqeQgDeUEjteXce9fzAHgmtnneqpzS9oD6hIQknck,2460
60
60
  mud/commands/give.py,sha256=xd9K05zxDlxVNHiQ2YzEC5OUCNKNULDHAfP1omMiu5s,10979
61
61
  mud/commands/group_commands.py,sha256=xQJUQqXqAJOfmfgv4fvhEioNM-ivlA5bwDsias_ncsA,22509
62
- mud/commands/healer.py,sha256=-H1IG5MskNLDX1m34v3yx6Low3gt-21AcmT9e7X97Ig,8893
62
+ mud/commands/healer.py,sha256=5R-LeJc1QuJWsBjqoYcQLCIaQ6bg264X2I1YHDkcHWY,9260
63
63
  mud/commands/help.py,sha256=J-oUmYD_6zyJ5RyqWk1M3Cipq6xJfGvpbZZe0mz678Q,11112
64
64
  mud/commands/imc.py,sha256=aFrmiJJyeYMvcfhnNVCtJtKs04k-88BaLgmPj-vE-8o,12179
65
65
  mud/commands/imm_admin.py,sha256=D4OczNuWVo5IQ74Jzz7j0Ypa6W09LyBPMDGmuwvQeHw,9279
@@ -72,10 +72,10 @@ mud/commands/imm_punish.py,sha256=zZPjJKbMbQ-saRPeE-27sI7UqgKUZP18gomNbyQh_X0,82
72
72
  mud/commands/imm_search.py,sha256=TFgWqqo145sZW8GfMq9wID5iwAOY06XkPbFfRYOxKtc,46570
73
73
  mud/commands/imm_server.py,sha256=JC4UkS8cDqmpQ91Dw34hfoQzgrLuvMZbgWXOLoltmAk,9148
74
74
  mud/commands/imm_set.py,sha256=gHHXzCVvmAlBxqor9p3eoDQGaZYIDebXyJqTfNxe9WE,18423
75
- mud/commands/info.py,sha256=yTq0lkG0CctjTLw8xMqTBuS31jZXFMqEj0Sv68vHZZw,20729
75
+ mud/commands/info.py,sha256=_TvpiXdOIX1tUaF9V0jDfTY2iDasDkYPJ_8imcx-guE,21135
76
76
  mud/commands/info_extended.py,sha256=EuGPJK4qjpyIaD3m2cOdwXUpcs9bAevi-JCeu2BDUj0,11365
77
- mud/commands/inspection.py,sha256=gdpBjcZP8nPLcxtBP51fjWfVzcK6mpEfyvWaf_YJbWA,10756
78
- mud/commands/inventory.py,sha256=EeBBDixkoVT9q_z7zRuRJrEbSn2pHe2WcwnzExeb4Vc,36698
77
+ mud/commands/inspection.py,sha256=5ii1iYJT9oee-WJUrBjnhPIEugCe5hJNoeqMCIFOTnA,11000
78
+ mud/commands/inventory.py,sha256=aWfh_LoMCj4R1kzPWejmAarWl-5Pr0UNGo5ZIceA68s,37772
79
79
  mud/commands/liquids.py,sha256=yeUzBIcVD9_K6QoeoBCaXipEZc1cYMbBkGP7aeZg-Pc,11546
80
80
  mud/commands/magic_items.py,sha256=YBymF8EyYdENBqBMf6ujtGqM2onSv-kgMgCRrwBrIhw,14134
81
81
  mud/commands/misc_info.py,sha256=gaaOEhV7hDLPJe2BUbxSAg-uFXjGfLfJxuLGgUeEDII,8350
@@ -84,14 +84,14 @@ mud/commands/mobprog_tools.py,sha256=DtfWBkzyYaXTqWoioupnkCH1Lf0mTfzvCZKlWQcSef4
84
84
  mud/commands/movement.py,sha256=fSXODjxubl9bbEq65RlDxdTYKknrvAQ_YRkcGAOUqYk,3327
85
85
  mud/commands/murder.py,sha256=mWW7AZRfrg86yVjc6EiXlXwApRC9ze5uryhUUXHyOGQ,5932
86
86
  mud/commands/notes.py,sha256=hoSBmAN-Po6KGb3H6Yc8sGIJ0FNsn6U1pDpzb70pdek,18155
87
- mud/commands/obj_manipulation.py,sha256=lj6K4u9hcxiC5BM2SUcqSzJhOfdw3xVTeuGl3XdRSzo,28628
87
+ mud/commands/obj_manipulation.py,sha256=ujye4CciOI0pDCXC8ds-7n-zPuW5Lb8eI8_2Zr5NbbI,28804
88
88
  mud/commands/player_config.py,sha256=XCIRNGlDVw7jqD1uHFzPPoWSuS7v_2OAtFKPt2tdL_0,6548
89
89
  mud/commands/player_info.py,sha256=aRCn3U-JmEka6zjp6dxRWvNTSF2XpuwVn2Ja6EhbRUg,7101
90
90
  mud/commands/position.py,sha256=Br_iqeo_84yJGghMoUY5p2bXqbetiekthj2iK8C4-to,16360
91
91
  mud/commands/remaining_rom.py,sha256=vGl3vBTfME99hhPPavQwUEempeaxr4g6Bwu2LzXDM_U,27959
92
- mud/commands/session.py,sha256=aYhYn2CDRR-ogtgs7dVQbYjuYObfxJ0pZdzeKRPtuGY,16249
92
+ mud/commands/session.py,sha256=pgBAlVSliH2J6M1HTKoAakDstPp7-zPHkGfUMnCx2Ic,16910
93
93
  mud/commands/shop.py,sha256=gynLzx08jrYZYiJJkHQeCABSgWkBvrrwTELJaYCYmMU,47657
94
- mud/commands/socials.py,sha256=KNE5LZfzamZBojg4WHxISZ2bYZOzb4VEVrY0z38qFqY,6219
94
+ mud/commands/socials.py,sha256=RNEzOw8xvxFUM9AoWSFhKeBOG-Opt-1DNiecgVkBUXQ,6492
95
95
  mud/commands/thief_skills.py,sha256=B9UqeUVQZqE16VqOyh1cTO86oXy9m6K1iqOKdeDmXII,16578
96
96
  mud/commands/typo_guards.py,sha256=SbvD9yZ6Z95qsCJN1CAQPtWNs6rsLH4W930_pc3SuoE,1435
97
97
  mud/db/__init__.py,sha256=DuOdj63wifJjPIR252Z1WpYDdJ5M7vqoQVI3MqrGhYY,29
@@ -188,7 +188,7 @@ mud/security/bans.py,sha256=_nM9wEJbxxdLXoe0I31u8dTYgWP0wxMqMVB9mxxfhlk,9174
188
188
  mud/security/hash_utils.py,sha256=GNd2W-TFLSIOk1WMQRuvJo8l4CueBYzPI0EhQuydxUg,656
189
189
  mud/skills/__init__.py,sha256=zzIr-ejbrlrQF1CSepODWhpxQZv8rO2pgaqIi3vpMg0,241
190
190
  mud/skills/groups.py,sha256=DRO7b-XPkP-DZudD-DVLMC74R0N6xXDFjceCrOct1Gw,8140
191
- mud/skills/handlers.py,sha256=UIQ_MYf_MODiNNHZasGRsTOBBzeoo81_dWVZUIsLpRc,298919
191
+ mud/skills/handlers.py,sha256=jzkgfFvdqmRr3xyVdvEhOPZ7FB7aozr3ZJ-gktDflKM,303843
192
192
  mud/skills/metadata.py,sha256=TbsOLZ1hKKAPTmrWBw4zKv1L3KCXByxpa2X2Rfir7G8,17480
193
193
  mud/skills/registry.py,sha256=4e8GD9-SUdn_6FhM7yFguJINX2vjKv2Y4Tp7_49Rves,14899
194
194
  mud/skills/say_spell.py,sha256=7Lcbx_oxA0JJQbaaILNb27xLbMGadauekyWHVnODiUM,4006
@@ -197,7 +197,7 @@ mud/spawning/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
197
197
  mud/spawning/mob_spawner.py,sha256=aQRuctCIPpkVZlES5NH0dsvphkCWJWFCrJB5dy7aMKY,577
198
198
  mud/spawning/obj_spawner.py,sha256=1jb1_sw7vvUVczXLi-dp5g_g6a-JVK2N4F4cuktiQF8,2930
199
199
  mud/spawning/reset_handler.py,sha256=Dfkszau017ReeUd26R9DOl6UWB5YGEOrwHGgSTcPRCQ,35099
200
- mud/spawning/templates.py,sha256=cVxjwgtXu-xoGPSXI3SkYeb9lAlK5Lvq0yozLfXrL0E,30451
200
+ mud/spawning/templates.py,sha256=hjQrZwFxDU4o3Tuja6DIdzBzTgTImUrbmV-13OLIsp8,31235
201
201
  mud/utils/act.py,sha256=385Tfm77Wz6FhsGBvxi9zd67cyhQJ6uku5TyiEY-HtE,14329
202
202
  mud/utils/bit.py,sha256=5W4uIoz_HVvUGBSyaO6JViuvseqO7s_1Gw0d2_y2m-4,4891
203
203
  mud/utils/fix_sex.py,sha256=_MH1NkDOFn33aTlwUU6e2VMmO8oQJibQNvcH8_w1PPk,601
@@ -213,15 +213,15 @@ mud/utils/timing.py,sha256=fK4LNOEZk5iLUhkmX_VTBgk3zldDBwLjI-_UEDWhmCU,670
213
213
  mud/world/__init__.py,sha256=HDEHtULHu1OWjOa3xglmhquW8L6NkJTWzLEURJ4BQjk,263
214
214
  mud/world/char_find.py,sha256=AwP7IRukVEHcZY6qtakWYsf-fFJjha5aTR7OiOtj3_s,5647
215
215
  mud/world/linking.py,sha256=mxEvgqA9Y6sDGXxN46aauU-jBhx4dOUcbgq64fIVz8g,1169
216
- mud/world/look.py,sha256=aeQApCN8jS_lWR5AwWAsVPpEi_fRfMINEV3aKy9SUHg,20361
216
+ mud/world/look.py,sha256=Q3iaX8S1Dax6XaTR2XagQci1MHoxGDQn0Zi27cLDJUQ,25178
217
217
  mud/world/movement.py,sha256=Rjcttco509exs42xvc1FkEnfJdF5qYX3BVIAaJoIcjE,26093
218
218
  mud/world/obj_find.py,sha256=wVF3KlcEm4Bc4GiQZs4yWH5Ih87F-JSM7MtVI8cZEJs,5277
219
219
  mud/world/time_persistence.py,sha256=GfhVr6H86Q4YXkeGhhgVAfm5s3ZBT8bF6K_zwqKRkAc,1663
220
220
  mud/world/vision.py,sha256=Z7uDNjer2C6-VxlQp35wJO-FpaRiUIj5D1IkLq0wZ6g,12965
221
221
  mud/world/world_state.py,sha256=58GTFAZ70d0Xp1FC8pU4PPJv5KlYI9Kmy_xeXoerfTo,9310
222
- rom24_quickmud_python-2.14.264.dist-info/licenses/LICENSE,sha256=anQ2j9As6sIC8tZgQCXbo0-09JDH9vPiqhA9djnOvkY,1078
223
- rom24_quickmud_python-2.14.264.dist-info/METADATA,sha256=62-ooa_PMFmZBltNtEzVpce3WDrtw1tcfftkcd-hlaw,19675
224
- rom24_quickmud_python-2.14.264.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
225
- rom24_quickmud_python-2.14.264.dist-info/entry_points.txt,sha256=VFru08UQTXZA_CkK-NBjJmWHyEX5a3864fQHjhaojbw,41
226
- rom24_quickmud_python-2.14.264.dist-info/top_level.txt,sha256=Fk1WPmabIIjp7_iZXLYpbAVqiq7lG7TeAHt30AsOKtQ,4
227
- rom24_quickmud_python-2.14.264.dist-info/RECORD,,
222
+ rom24_quickmud_python-2.14.298.dist-info/licenses/LICENSE,sha256=anQ2j9As6sIC8tZgQCXbo0-09JDH9vPiqhA9djnOvkY,1078
223
+ rom24_quickmud_python-2.14.298.dist-info/METADATA,sha256=kjZi7RVkj_GDL2WQoC_T_dm1NnWJUvtyvBtC2B6rWKE,19675
224
+ rom24_quickmud_python-2.14.298.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
225
+ rom24_quickmud_python-2.14.298.dist-info/entry_points.txt,sha256=VFru08UQTXZA_CkK-NBjJmWHyEX5a3864fQHjhaojbw,41
226
+ rom24_quickmud_python-2.14.298.dist-info/top_level.txt,sha256=Fk1WPmabIIjp7_iZXLYpbAVqiq7lG7TeAHt30AsOKtQ,4
227
+ rom24_quickmud_python-2.14.298.dist-info/RECORD,,