rom24-quickmud-python 2.5.2__py3-none-any.whl → 2.5.4__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/__main__.py +1 -1
- mud/ai/__init__.py +14 -8
- mud/characters/follow.py +15 -1
- mud/combat/death.py +22 -26
- mud/combat/engine.py +57 -37
- mud/commands/admin_commands.py +2 -0
- mud/commands/advancement.py +10 -2
- mud/commands/affects.py +96 -45
- mud/commands/character.py +99 -53
- mud/commands/combat.py +10 -0
- mud/commands/communication.py +15 -4
- mud/commands/compare.py +59 -92
- mud/commands/equipment.py +153 -30
- mud/commands/imm_commands.py +92 -86
- mud/commands/info.py +328 -89
- mud/commands/info_extended.py +127 -67
- mud/commands/inspection.py +147 -9
- mud/commands/inventory.py +169 -9
- mud/commands/obj_manipulation.py +39 -3
- mud/commands/player_info.py +42 -40
- mud/commands/session.py +166 -24
- mud/commands/shop.py +3 -24
- mud/config.py +19 -1
- mud/game_loop.py +183 -11
- mud/handler.py +1424 -0
- mud/magic/effects.py +723 -34
- mud/mob_cmds.py +54 -10
- mud/models/character.py +178 -2
- mud/models/constants.py +8 -0
- mud/models/room.py +77 -7
- mud/persistence.py +327 -1
- mud/skills/handlers.py +0 -8
- mud/spawning/templates.py +73 -4
- mud/utils/rng_mm.py +15 -0
- mud/utils/text.py +50 -12
- mud/world/char_find.py +33 -35
- mud/world/look.py +156 -49
- mud/world/obj_find.py +60 -40
- mud/world/vision.py +17 -7
- mud/world/world_state.py +13 -2
- {rom24_quickmud_python-2.5.2.dist-info → rom24_quickmud_python-2.5.4.dist-info}/METADATA +13 -11
- {rom24_quickmud_python-2.5.2.dist-info → rom24_quickmud_python-2.5.4.dist-info}/RECORD +46 -45
- {rom24_quickmud_python-2.5.2.dist-info → rom24_quickmud_python-2.5.4.dist-info}/WHEEL +0 -0
- {rom24_quickmud_python-2.5.2.dist-info → rom24_quickmud_python-2.5.4.dist-info}/entry_points.txt +0 -0
- {rom24_quickmud_python-2.5.2.dist-info → rom24_quickmud_python-2.5.4.dist-info}/licenses/LICENSE +0 -0
- {rom24_quickmud_python-2.5.2.dist-info → rom24_quickmud_python-2.5.4.dist-info}/top_level.txt +0 -0
mud/__main__.py
CHANGED
mud/ai/__init__.py
CHANGED
|
@@ -16,7 +16,8 @@ from mud.models.constants import (
|
|
|
16
16
|
EX_CLOSED,
|
|
17
17
|
)
|
|
18
18
|
from mud.models.obj import ObjectData
|
|
19
|
-
from mud.models.room import Exit, Room
|
|
19
|
+
from mud.models.room import Exit, Room
|
|
20
|
+
from mud.registry import room_registry
|
|
20
21
|
from mud.utils import rng_mm
|
|
21
22
|
from mud.world.movement import move_character
|
|
22
23
|
|
|
@@ -209,14 +210,20 @@ def _room_contents(room: Room | None) -> Iterable[ObjectData]:
|
|
|
209
210
|
|
|
210
211
|
|
|
211
212
|
def _take_object(mob: Character, obj: ObjectData) -> None:
|
|
212
|
-
room = getattr(obj, "in_room", None) or getattr(mob, "room", None)
|
|
213
|
+
room = getattr(obj, "in_room", None) or getattr(obj, "location", None) or getattr(mob, "room", None)
|
|
213
214
|
if room is not None:
|
|
214
215
|
contents = getattr(room, "contents", None)
|
|
215
216
|
if isinstance(contents, list) and obj in contents:
|
|
216
217
|
contents.remove(obj)
|
|
217
|
-
|
|
218
|
-
obj
|
|
219
|
-
|
|
218
|
+
|
|
219
|
+
if hasattr(obj, "in_room"):
|
|
220
|
+
obj.in_room = None
|
|
221
|
+
if hasattr(obj, "location"):
|
|
222
|
+
obj.location = None
|
|
223
|
+
if hasattr(obj, "in_obj"):
|
|
224
|
+
obj.in_obj = None
|
|
225
|
+
if hasattr(obj, "carried_by"):
|
|
226
|
+
obj.carried_by = mob
|
|
220
227
|
|
|
221
228
|
inventory = getattr(mob, "inventory", None)
|
|
222
229
|
if isinstance(inventory, list) and obj not in inventory:
|
|
@@ -280,9 +287,8 @@ def _maybe_wander(mob: Character, room: Room) -> None:
|
|
|
280
287
|
if rng_mm.number_bits(3) != 0:
|
|
281
288
|
return
|
|
282
289
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return
|
|
290
|
+
# ROM C mob_cmds.c:1274 uses number_door() for random direction
|
|
291
|
+
door = rng_mm.number_door()
|
|
286
292
|
|
|
287
293
|
exit_obj = _valid_exit(room, door)
|
|
288
294
|
if exit_obj is None:
|
mud/characters/follow.py
CHANGED
|
@@ -67,4 +67,18 @@ def stop_follower(follower: "Character") -> None:
|
|
|
67
67
|
follower.leader = None
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
def die_follower(char: "Character") -> None:
|
|
71
|
+
"""Stop all followers when character dies.
|
|
72
|
+
|
|
73
|
+
ROM Reference: src/handler.c die_follower
|
|
74
|
+
Mirrors ROM behavior: when character dies, all their followers stop following.
|
|
75
|
+
"""
|
|
76
|
+
from mud.models.character import character_registry
|
|
77
|
+
|
|
78
|
+
for follower in list(character_registry):
|
|
79
|
+
master = getattr(follower, "master", None)
|
|
80
|
+
if master is char:
|
|
81
|
+
stop_follower(follower)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
__all__ = ["add_follower", "stop_follower", "die_follower"]
|
mud/combat/death.py
CHANGED
|
@@ -145,7 +145,9 @@ def _is_floating_slot(slot: str | None, obj: Object) -> bool:
|
|
|
145
145
|
|
|
146
146
|
|
|
147
147
|
def _format_corpse_labels(corpse: Object, name: str) -> None:
|
|
148
|
-
short_template = getattr(corpse.prototype, "short_descr", None) or getattr(
|
|
148
|
+
short_template = getattr(corpse.prototype, "short_descr", None) or getattr(
|
|
149
|
+
corpse, "short_descr", "the corpse of %s"
|
|
150
|
+
)
|
|
149
151
|
desc_template = getattr(corpse.prototype, "description", None) or getattr(
|
|
150
152
|
corpse, "description", "The corpse of %s is lying here."
|
|
151
153
|
)
|
|
@@ -216,11 +218,7 @@ def _broadcast_neighbor_cry(victim: Character) -> None:
|
|
|
216
218
|
if room is None:
|
|
217
219
|
return
|
|
218
220
|
|
|
219
|
-
message = (
|
|
220
|
-
"You hear something's death cry."
|
|
221
|
-
if getattr(victim, "is_npc", False)
|
|
222
|
-
else "You hear someone's death cry."
|
|
223
|
-
)
|
|
221
|
+
message = "You hear something's death cry." if getattr(victim, "is_npc", False) else "You hear someone's death cry."
|
|
224
222
|
|
|
225
223
|
for exit_data in getattr(room, "exits", []) or []:
|
|
226
224
|
if exit_data is None:
|
|
@@ -341,7 +339,7 @@ def death_cry(victim: Character) -> None:
|
|
|
341
339
|
def _fallback_corpse(vnum: int, *, item_type: ItemType) -> Object:
|
|
342
340
|
"""Return a minimal corpse object when the real prototype is missing."""
|
|
343
341
|
|
|
344
|
-
proto = ObjIndex(vnum=vnum, short_descr="
|
|
342
|
+
proto = ObjIndex(vnum=vnum, short_descr="the corpse of %s", description="The corpse of %s is lying here.")
|
|
345
343
|
proto.item_type = int(item_type)
|
|
346
344
|
corpse = Object(instance_id=None, prototype=proto)
|
|
347
345
|
corpse.item_type = int(item_type)
|
|
@@ -368,19 +366,6 @@ def _strip_inventory(victim: Character) -> list[tuple[Object, bool]]:
|
|
|
368
366
|
return items
|
|
369
367
|
|
|
370
368
|
|
|
371
|
-
def _set_corpse_coins(corpse: Object, gold: int, silver: int) -> None:
|
|
372
|
-
"""Persist coin totals on the corpse and mirror into ``value[0:2]``."""
|
|
373
|
-
|
|
374
|
-
corpse.gold = gold
|
|
375
|
-
corpse.silver = silver
|
|
376
|
-
values = list(getattr(corpse, "value", []) or [])
|
|
377
|
-
while len(values) < 2:
|
|
378
|
-
values.append(0)
|
|
379
|
-
values[0] = gold
|
|
380
|
-
values[1] = silver
|
|
381
|
-
corpse.value = values
|
|
382
|
-
|
|
383
|
-
|
|
384
369
|
def _handle_corpse_item(
|
|
385
370
|
corpse: Object,
|
|
386
371
|
room,
|
|
@@ -445,9 +430,18 @@ def make_corpse(victim: Character) -> Object | None:
|
|
|
445
430
|
|
|
446
431
|
gold = max(0, int(getattr(victim, "gold", 0) or 0))
|
|
447
432
|
silver = max(0, int(getattr(victim, "silver", 0) or 0))
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
433
|
+
|
|
434
|
+
# ROM C fight.c:1473-1478 - Create money object inside corpse
|
|
435
|
+
if gold > 0 or silver > 0:
|
|
436
|
+
from mud.handler import create_money
|
|
437
|
+
|
|
438
|
+
money_obj = create_money(gold, silver)
|
|
439
|
+
if money_obj:
|
|
440
|
+
corpse.contained_items.append(money_obj)
|
|
441
|
+
money_obj.location = None # Inside corpse, not in room
|
|
442
|
+
|
|
443
|
+
victim.gold = 0
|
|
444
|
+
victim.silver = 0
|
|
451
445
|
|
|
452
446
|
if not is_npc:
|
|
453
447
|
_clear_player_flag(victim, PlayerFlag.CANLOOT)
|
|
@@ -557,16 +551,18 @@ def raw_kill(victim: Character) -> Object | None:
|
|
|
557
551
|
"""Handle character death by creating a corpse and removing the victim."""
|
|
558
552
|
|
|
559
553
|
from mud.combat.engine import stop_fighting as _stop_fighting
|
|
560
|
-
|
|
554
|
+
from mud.characters.follow import die_follower
|
|
555
|
+
|
|
561
556
|
# Trigger death mobprog handled in apply_damage before raw_kill
|
|
562
557
|
# ROM Reference: src/fight.c:1136-1180 (mp_death_trigger called before raw_kill)
|
|
563
|
-
|
|
558
|
+
|
|
559
|
+
_nuke_pets(victim, room=getattr(victim, "room", None))
|
|
560
|
+
die_follower(victim)
|
|
564
561
|
_stop_fighting(victim, True)
|
|
565
562
|
death_cry(victim)
|
|
566
563
|
corpse = make_corpse(victim)
|
|
567
564
|
|
|
568
565
|
room = getattr(victim, "room", None)
|
|
569
|
-
_nuke_pets(victim, room)
|
|
570
566
|
if room is not None:
|
|
571
567
|
room.remove_character(victim)
|
|
572
568
|
|
mud/combat/engine.py
CHANGED
|
@@ -499,6 +499,20 @@ def apply_damage(
|
|
|
499
499
|
if victim.position == Position.DEAD:
|
|
500
500
|
return "Already dead."
|
|
501
501
|
|
|
502
|
+
# Set up fighting state BEFORE defense checks (ROM parity: src/fight.c:damage sets fighting before parry/dodge)
|
|
503
|
+
if victim != attacker:
|
|
504
|
+
if victim.position > Position.STUNNED:
|
|
505
|
+
if victim.fighting is None:
|
|
506
|
+
set_fighting(victim, attacker)
|
|
507
|
+
if getattr(victim, "is_npc", False):
|
|
508
|
+
mobprog.mp_kill_trigger(victim, attacker)
|
|
509
|
+
if getattr(victim, "timer", 0) <= 4:
|
|
510
|
+
victim.position = Position.FIGHTING
|
|
511
|
+
|
|
512
|
+
if victim.position > Position.STUNNED:
|
|
513
|
+
if attacker.fighting is None:
|
|
514
|
+
set_fighting(attacker, victim)
|
|
515
|
+
|
|
502
516
|
# Check for parry, dodge, and shield block following C src/fight.c:damage() order
|
|
503
517
|
# These are checked AFTER hit determination but BEFORE damage application
|
|
504
518
|
# Order is critical: shield_block → parry → dodge (per ROM C src/fight.c:one_hit)
|
|
@@ -533,23 +547,6 @@ def apply_damage(
|
|
|
533
547
|
message_bundle = dam_message(attacker, victim, damage, dt, immune)
|
|
534
548
|
_dispatch_damage_messages(attacker, victim, message_bundle)
|
|
535
549
|
|
|
536
|
-
# Set up fighting state if not already fighting
|
|
537
|
-
if victim != attacker:
|
|
538
|
-
# Victim starts fighting back if able
|
|
539
|
-
if victim.position > Position.STUNNED:
|
|
540
|
-
if victim.fighting is None:
|
|
541
|
-
set_fighting(victim, attacker)
|
|
542
|
-
if getattr(victim, "is_npc", False):
|
|
543
|
-
mobprog.mp_kill_trigger(victim, attacker)
|
|
544
|
-
# Update victim to fighting position if timer allows
|
|
545
|
-
if getattr(victim, "timer", 0) <= 4:
|
|
546
|
-
victim.position = Position.FIGHTING
|
|
547
|
-
|
|
548
|
-
# Attacker starts fighting if not already
|
|
549
|
-
if victim.position > Position.STUNNED:
|
|
550
|
-
if attacker.fighting is None:
|
|
551
|
-
set_fighting(attacker, victim)
|
|
552
|
-
|
|
553
550
|
if damage <= 0:
|
|
554
551
|
if message_bundle and message_bundle.attacker:
|
|
555
552
|
return message_bundle.attacker
|
|
@@ -752,30 +749,53 @@ def _object_has_wear_flag(obj, flag: WearFlag) -> bool:
|
|
|
752
749
|
|
|
753
750
|
|
|
754
751
|
def _transfer_corpse_coins(attacker: Character, corpse) -> bool:
|
|
755
|
-
"""
|
|
752
|
+
"""Extract gold/silver from money objects and add to attacker's purse.
|
|
756
753
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
754
|
+
ROM Reference: src/fight.c auto_loot handles money extraction.
|
|
755
|
+
QuickMUD uses actual money objects (ItemType.MONEY) instead of corpse attributes.
|
|
756
|
+
Money objects may already be in attacker's inventory if AUTOLOOT moved them.
|
|
757
|
+
"""
|
|
758
|
+
from mud.models.constants import ItemType
|
|
759
|
+
|
|
760
|
+
contained = list(getattr(corpse, "contained_items", []) or [])
|
|
761
|
+
attacker_inv = list(getattr(attacker, "inventory", []) or [])
|
|
765
762
|
|
|
766
|
-
|
|
763
|
+
money_in_corpse = [obj for obj in contained if getattr(obj, "item_type", None) == int(ItemType.MONEY)]
|
|
764
|
+
money_in_inventory = [obj for obj in attacker_inv if getattr(obj, "item_type", None) == int(ItemType.MONEY)]
|
|
765
|
+
|
|
766
|
+
all_money_objects = money_in_corpse + money_in_inventory
|
|
767
|
+
|
|
768
|
+
if not all_money_objects:
|
|
767
769
|
return False
|
|
768
770
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
771
|
+
total_gold = 0
|
|
772
|
+
total_silver = 0
|
|
773
|
+
|
|
774
|
+
for money_obj in all_money_objects:
|
|
775
|
+
values = list(getattr(money_obj, "value", [0, 0]) or [0, 0])
|
|
776
|
+
while len(values) < 2:
|
|
777
|
+
values.append(0)
|
|
778
|
+
|
|
779
|
+
silver = int(values[0] or 0)
|
|
780
|
+
gold = int(values[1] or 0)
|
|
781
|
+
|
|
782
|
+
total_silver += silver
|
|
783
|
+
total_gold += gold
|
|
784
|
+
|
|
785
|
+
if money_obj in money_in_corpse:
|
|
786
|
+
try:
|
|
787
|
+
corpse.contained_items.remove(money_obj)
|
|
788
|
+
except (AttributeError, ValueError):
|
|
789
|
+
pass
|
|
790
|
+
else:
|
|
791
|
+
try:
|
|
792
|
+
attacker.remove_object(money_obj)
|
|
793
|
+
except (AttributeError, ValueError):
|
|
794
|
+
pass
|
|
795
|
+
|
|
796
|
+
attacker.gold = int(getattr(attacker, "gold", 0) or 0) + total_gold
|
|
797
|
+
attacker.silver = int(getattr(attacker, "silver", 0) or 0) + total_silver
|
|
798
|
+
|
|
779
799
|
return True
|
|
780
800
|
|
|
781
801
|
|
mud/commands/admin_commands.py
CHANGED
|
@@ -34,6 +34,8 @@ def cmd_who(char: Character, args: str) -> str:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def cmd_teleport(char: Character, args: str) -> str:
|
|
37
|
+
if char.level < 52:
|
|
38
|
+
return "You don't have permission to use this command."
|
|
37
39
|
if not args.isdigit() or int(args) not in room_registry:
|
|
38
40
|
return "Invalid room."
|
|
39
41
|
target = room_registry[int(args)]
|
mud/commands/advancement.py
CHANGED
|
@@ -188,9 +188,17 @@ def do_practice(char: Character, args: str) -> str:
|
|
|
188
188
|
new_value = min(adept, current + increment)
|
|
189
189
|
char.skills[skill_key] = new_value
|
|
190
190
|
|
|
191
|
+
# ROM C parity: Send messages to both char and room (src/act_info.c:2767-2777)
|
|
191
192
|
if new_value >= adept:
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
char.messages.append(f"You are now learned at {skill.name}.")
|
|
194
|
+
if char.room:
|
|
195
|
+
char.room.broadcast(f"{char.name} is now learned at {skill.name}.", exclude=char)
|
|
196
|
+
else:
|
|
197
|
+
char.messages.append(f"You practice {skill.name}.")
|
|
198
|
+
if char.room:
|
|
199
|
+
char.room.broadcast(f"{char.name} practices {skill.name}.", exclude=char)
|
|
200
|
+
|
|
201
|
+
return ""
|
|
194
202
|
|
|
195
203
|
|
|
196
204
|
def do_train(char: Character, args: str) -> str:
|
mud/commands/affects.py
CHANGED
|
@@ -3,6 +3,7 @@ Affects command - show active spell/affect effects on character.
|
|
|
3
3
|
|
|
4
4
|
ROM Reference: src/act_info.c do_affects (lines 2300-2400)
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
9
|
from mud.models.character import Character
|
|
@@ -43,56 +44,106 @@ _AFFECT_NAMES = {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
def affect_loc_name(location: int) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Return ASCII name of an affect location.
|
|
50
|
+
|
|
51
|
+
ROM Reference: src/handler.c affect_loc_name (lines 2718-2775)
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
location: APPLY_* constant (0-25)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Human-readable location name (e.g., "strength", "armor class", "hit roll")
|
|
58
|
+
"""
|
|
59
|
+
# ROM C APPLY_* constants (src/merc.h lines 1205-1231)
|
|
60
|
+
APPLY_NAMES = {
|
|
61
|
+
0: "none", # APPLY_NONE
|
|
62
|
+
1: "strength", # APPLY_STR
|
|
63
|
+
2: "dexterity", # APPLY_DEX
|
|
64
|
+
3: "intelligence", # APPLY_INT
|
|
65
|
+
4: "wisdom", # APPLY_WIS
|
|
66
|
+
5: "constitution", # APPLY_CON
|
|
67
|
+
6: "sex", # APPLY_SEX
|
|
68
|
+
7: "class", # APPLY_CLASS
|
|
69
|
+
8: "level", # APPLY_LEVEL
|
|
70
|
+
9: "age", # APPLY_AGE
|
|
71
|
+
10: "height", # APPLY_HEIGHT (not shown in ROM affect_loc_name)
|
|
72
|
+
11: "weight", # APPLY_WEIGHT (not shown in ROM affect_loc_name)
|
|
73
|
+
12: "mana", # APPLY_MANA
|
|
74
|
+
13: "hp", # APPLY_HIT
|
|
75
|
+
14: "moves", # APPLY_MOVE
|
|
76
|
+
15: "gold", # APPLY_GOLD
|
|
77
|
+
16: "experience", # APPLY_EXP
|
|
78
|
+
17: "armor class", # APPLY_AC
|
|
79
|
+
18: "hit roll", # APPLY_HITROLL
|
|
80
|
+
19: "damage roll", # APPLY_DAMROLL
|
|
81
|
+
20: "saves", # APPLY_SAVES / APPLY_SAVING_PARA
|
|
82
|
+
21: "save vs rod", # APPLY_SAVING_ROD
|
|
83
|
+
22: "save vs petrification", # APPLY_SAVING_PETRI
|
|
84
|
+
23: "save vs breath", # APPLY_SAVING_BREATH
|
|
85
|
+
24: "save vs spell", # APPLY_SAVING_SPELL
|
|
86
|
+
25: "none", # APPLY_SPELL_AFFECT (returns "none" in ROM C)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return APPLY_NAMES.get(location, "(unknown)")
|
|
90
|
+
|
|
91
|
+
|
|
46
92
|
def do_affects(char: Character, args: str) -> str:
|
|
47
93
|
"""
|
|
48
94
|
Display active affects on the character.
|
|
49
|
-
|
|
50
|
-
ROM Reference: src/act_info.c do_affects (lines
|
|
51
|
-
|
|
95
|
+
|
|
96
|
+
ROM Reference: src/act_info.c do_affects (lines 1714-1755)
|
|
97
|
+
|
|
52
98
|
Usage: affects
|
|
99
|
+
|
|
100
|
+
Behavior:
|
|
101
|
+
- Level <20: Shows simple format (spell name only)
|
|
102
|
+
- Level 20+: Shows detailed format (modifier, location, duration)
|
|
103
|
+
- Stacked affects (same spell, multiple modifiers): Indented continuation lines
|
|
53
104
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
105
|
+
# Primary ROM C behavior: iterate ch.affected list (AFFECT_DATA structures)
|
|
106
|
+
affected = getattr(char, "affected", [])
|
|
107
|
+
|
|
108
|
+
if not affected:
|
|
109
|
+
return "You are not affected by any spells."
|
|
110
|
+
|
|
111
|
+
lines = ["You are affected by the following spells:"]
|
|
112
|
+
paf_last = None
|
|
113
|
+
|
|
114
|
+
for paf in affected:
|
|
115
|
+
# Deduplication: check if same spell as previous affect
|
|
116
|
+
if paf_last and paf.type == paf_last.type:
|
|
117
|
+
if char.level >= 20:
|
|
118
|
+
# Level 20+: Show duplicate affects with indentation (22 spaces + ": ")
|
|
119
|
+
buf = " " * 22 + ": "
|
|
64
120
|
else:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Check affect bitvector for built-in affects
|
|
68
|
-
affected_by = getattr(char, "affected_by", 0)
|
|
69
|
-
if affected_by:
|
|
70
|
-
if not spell_effects:
|
|
71
|
-
lines.append("You are affected by:")
|
|
121
|
+
# Level <20: Skip duplicate spells entirely
|
|
122
|
+
continue
|
|
72
123
|
else:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
124
|
+
# New spell: show spell name (left-aligned, 15 chars)
|
|
125
|
+
# TODO: Replace with proper skill_table[paf.type].name lookup when SN mapping is available
|
|
126
|
+
# For now, assume paf.type is already a skill name string (temporary until spell system updated)
|
|
127
|
+
spell_name = str(paf.type) if paf.type else "(unknown)"
|
|
128
|
+
|
|
129
|
+
buf = f"Spell: {spell_name:15s}"
|
|
130
|
+
|
|
131
|
+
# Level 20+: Show detailed modifier information
|
|
132
|
+
if char.level >= 20:
|
|
133
|
+
location_name = affect_loc_name(paf.location)
|
|
134
|
+
|
|
135
|
+
# ROM C line 1737: uses raw %d (no explicit + sign)
|
|
136
|
+
modifier_str = str(paf.modifier)
|
|
137
|
+
|
|
138
|
+
if paf.duration == -1:
|
|
139
|
+
duration_str = "permanently"
|
|
140
|
+
else:
|
|
141
|
+
duration_str = f"for {paf.duration} hours"
|
|
142
|
+
|
|
143
|
+
# ROM C line 1736: ": modifies..." (colon prefix)
|
|
144
|
+
buf += f": modifies {location_name} by {modifier_str} {duration_str}"
|
|
145
|
+
|
|
146
|
+
lines.append(buf)
|
|
147
|
+
paf_last = paf
|
|
148
|
+
|
|
98
149
|
return "\n".join(lines)
|