rom24-quickmud-python 2.0.5__py3-none-any.whl → 2.1.1__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/account_manager.py +46 -3
- mud/account/account_service.py +23 -54
- mud/advancement.py +2 -1
- mud/combat/engine.py +15 -33
- mud/combat/safety.py +106 -0
- mud/commands/admin_commands.py +1 -1
- mud/commands/affects.py +98 -0
- mud/commands/auto_settings.py +340 -0
- mud/commands/build.py +1388 -12
- mud/commands/channels.py +51 -0
- mud/commands/character.py +159 -0
- mud/commands/combat.py +421 -0
- mud/commands/communication.py +87 -8
- mud/commands/compare.py +144 -0
- mud/commands/consider.py +68 -0
- mud/commands/consumption.py +257 -0
- mud/commands/dispatcher.py +360 -12
- mud/commands/doors.py +508 -0
- mud/commands/equipment.py +306 -0
- mud/commands/feedback.py +93 -0
- mud/commands/give.py +196 -0
- mud/commands/group_commands.py +393 -0
- mud/commands/help.py +28 -11
- mud/commands/imc.py +12 -10
- mud/commands/imm_admin.py +280 -0
- mud/commands/imm_commands.py +457 -0
- mud/commands/imm_display.py +281 -0
- mud/commands/imm_emote.py +156 -0
- mud/commands/imm_load.py +382 -0
- mud/commands/imm_olc.py +254 -0
- mud/commands/imm_punish.py +259 -0
- mud/commands/imm_search.py +593 -0
- mud/commands/imm_server.py +239 -0
- mud/commands/imm_set.py +500 -0
- mud/commands/info.py +277 -0
- mud/commands/info_extended.py +315 -0
- mud/commands/inspection.py +28 -3
- mud/commands/inventory.py +61 -3
- mud/commands/liquids.py +246 -0
- mud/commands/magic_items.py +451 -0
- mud/commands/misc_info.py +270 -0
- mud/commands/misc_player.py +274 -0
- mud/commands/mobprog_tools.py +40 -0
- mud/commands/murder.py +122 -0
- mud/commands/obj_manipulation.py +501 -0
- mud/commands/player_config.py +216 -0
- mud/commands/player_info.py +181 -0
- mud/commands/position.py +85 -0
- mud/commands/remaining_rom.py +552 -0
- mud/commands/session.py +267 -0
- mud/commands/shop.py +19 -29
- mud/commands/thief_skills.py +351 -0
- mud/commands/typo_guards.py +86 -0
- mud/db/migrations.py +24 -2
- mud/db/models.py +3 -0
- mud/game_loop.py +46 -18
- mud/loaders/__init__.py +15 -2
- mud/loaders/area_loader.py +27 -54
- mud/loaders/base_loader.py +13 -3
- mud/loaders/help_loader.py +9 -2
- mud/loaders/json_loader.py +3 -0
- mud/mobprog.py +131 -0
- mud/models/character.py +24 -3
- mud/net/ansi.py +2 -0
- mud/net/connection.py +158 -136
- mud/net/telnet_server.py +17 -3
- mud/network/websocket_server.py +22 -0
- mud/olc/__init__.py +11 -0
- mud/olc/save.py +293 -0
- mud/persistence.py +9 -0
- mud/rom_api.py +677 -0
- mud/scripts/convert_are_to_json.py +2 -1
- mud/scripts/convert_help_are_to_json.py +42 -2
- mud/scripts/convert_player_to_json.py +28 -0
- mud/skills/handlers.py +1299 -266
- mud/spawning/reset_handler.py +17 -7
- mud/wiznet.py +3 -2
- mud/world/char_find.py +134 -0
- mud/world/look.py +243 -2
- mud/world/movement.py +7 -16
- mud/world/obj_find.py +180 -0
- {rom24_quickmud_python-2.0.5.dist-info → rom24_quickmud_python-2.1.1.dist-info}/METADATA +18 -8
- {rom24_quickmud_python-2.0.5.dist-info → rom24_quickmud_python-2.1.1.dist-info}/RECORD +87 -45
- {rom24_quickmud_python-2.0.5.dist-info → rom24_quickmud_python-2.1.1.dist-info}/WHEEL +0 -0
- {rom24_quickmud_python-2.0.5.dist-info → rom24_quickmud_python-2.1.1.dist-info}/entry_points.txt +0 -0
- {rom24_quickmud_python-2.0.5.dist-info → rom24_quickmud_python-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {rom24_quickmud_python-2.0.5.dist-info → rom24_quickmud_python-2.1.1.dist-info}/top_level.txt +0 -0
mud/account/account_manager.py
CHANGED
|
@@ -44,15 +44,46 @@ def save_character(character: Character) -> None:
|
|
|
44
44
|
try:
|
|
45
45
|
session = SessionLocal()
|
|
46
46
|
db_char = session.query(DBCharacter).filter_by(name=character.name).first()
|
|
47
|
+
if not db_char:
|
|
48
|
+
# Character doesn't exist in database - create it
|
|
49
|
+
# This handles cases where character was created via JSON or other means
|
|
50
|
+
print(f"[WARN] Character '{character.name}' not found in database, creating new record")
|
|
51
|
+
|
|
52
|
+
# CRITICAL: Try to find and link the player account
|
|
53
|
+
player_id = None
|
|
54
|
+
pcdata = getattr(character, "pcdata", None)
|
|
55
|
+
if pcdata:
|
|
56
|
+
account_name = getattr(pcdata, "account_name", None)
|
|
57
|
+
if account_name:
|
|
58
|
+
player_account = session.query(PlayerAccount).filter_by(username=account_name).first()
|
|
59
|
+
if player_account:
|
|
60
|
+
player_id = player_account.id
|
|
61
|
+
print(f"[INFO] Linked character '{character.name}' to account '{account_name}' (id={player_id})")
|
|
62
|
+
else:
|
|
63
|
+
print(f"[WARN] Could not find player account '{account_name}' for character '{character.name}'")
|
|
64
|
+
|
|
65
|
+
db_char = DBCharacter(name=character.name, player_id=player_id)
|
|
66
|
+
session.add(db_char)
|
|
67
|
+
|
|
68
|
+
# Update all fields
|
|
47
69
|
if db_char:
|
|
70
|
+
# Ensure player_id is set if we have account information
|
|
71
|
+
if not db_char.player_id:
|
|
72
|
+
pcdata = getattr(character, "pcdata", None)
|
|
73
|
+
if pcdata:
|
|
74
|
+
account_name = getattr(pcdata, "account_name", None)
|
|
75
|
+
if account_name:
|
|
76
|
+
player_account = session.query(PlayerAccount).filter_by(username=account_name).first()
|
|
77
|
+
if player_account:
|
|
78
|
+
db_char.player_id = player_account.id
|
|
79
|
+
print(f"[INFO] Fixed missing player_id for character '{character.name}' -> account '{account_name}'")
|
|
80
|
+
|
|
48
81
|
db_char.level = character.level
|
|
49
82
|
db_char.hp = character.hit
|
|
50
83
|
db_char.race = int(character.race or 0)
|
|
51
84
|
db_char.ch_class = int(character.ch_class or 0)
|
|
52
85
|
pcdata = getattr(character, "pcdata", None)
|
|
53
|
-
true_sex_value = int(
|
|
54
|
-
getattr(pcdata, "true_sex", getattr(character, "sex", 0)) or 0
|
|
55
|
-
)
|
|
86
|
+
true_sex_value = int(getattr(pcdata, "true_sex", getattr(character, "sex", 0)) or 0)
|
|
56
87
|
db_char.true_sex = true_sex_value
|
|
57
88
|
db_char.sex = int(character.sex or true_sex_value or 0)
|
|
58
89
|
db_char.alignment = int(character.alignment or 0)
|
|
@@ -67,6 +98,18 @@ def save_character(character: Character) -> None:
|
|
|
67
98
|
db_char.vuln_flags = int(character.vuln_flags or 0)
|
|
68
99
|
db_char.practice = int(character.practice or 0)
|
|
69
100
|
db_char.train = int(character.train or 0)
|
|
101
|
+
|
|
102
|
+
# Save perm stats from pcdata (ROM src/handler.c stores perm_hit/perm_mana/perm_move)
|
|
103
|
+
if pcdata:
|
|
104
|
+
db_char.perm_hit = int(getattr(pcdata, "perm_hit", character.max_hit or 20))
|
|
105
|
+
db_char.perm_mana = int(getattr(pcdata, "perm_mana", character.max_mana or 100))
|
|
106
|
+
db_char.perm_move = int(getattr(pcdata, "perm_move", character.max_move or 100))
|
|
107
|
+
else:
|
|
108
|
+
# Fallback if no pcdata
|
|
109
|
+
db_char.perm_hit = int(character.max_hit or 20)
|
|
110
|
+
db_char.perm_mana = int(character.max_mana or 100)
|
|
111
|
+
db_char.perm_move = int(character.max_move or 100)
|
|
112
|
+
|
|
70
113
|
db_char.default_weapon_vnum = int(character.default_weapon_vnum or 0)
|
|
71
114
|
db_char.creation_points = int(getattr(character, "creation_points", 0) or 0)
|
|
72
115
|
db_char.creation_groups = json.dumps(list(getattr(character, "creation_groups", ())))
|
mud/account/account_service.py
CHANGED
|
@@ -80,9 +80,7 @@ _RESERVED_NAMES = {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
_HOMETOWN_CHOICES: Final[tuple[tuple[str, int], ...]] = (
|
|
84
|
-
("Midgaard", ROOM_VNUM_SCHOOL),
|
|
85
|
-
)
|
|
83
|
+
_HOMETOWN_CHOICES: Final[tuple[tuple[str, int], ...]] = (("Midgaard", ROOM_VNUM_SCHOOL),)
|
|
86
84
|
|
|
87
85
|
_DEFAULT_WEAPONS: Final[tuple[str, ...]] = ("dagger",)
|
|
88
86
|
|
|
@@ -377,9 +375,7 @@ class CreationSelection:
|
|
|
377
375
|
cost = self._chosen_skills.pop(normalized, 0)
|
|
378
376
|
if cost > 0:
|
|
379
377
|
self.creation_points = max(self._base_points, self.creation_points - cost)
|
|
380
|
-
self._skill_order = [
|
|
381
|
-
entry for entry in self._skill_order if self._normalize(entry) != normalized
|
|
382
|
-
]
|
|
378
|
+
self._skill_order = [entry for entry in self._skill_order if self._normalize(entry) != normalized]
|
|
383
379
|
if not sources:
|
|
384
380
|
self._skill_sources.pop(normalized, None)
|
|
385
381
|
self._known_skills.discard(normalized)
|
|
@@ -396,11 +392,7 @@ class CreationSelection:
|
|
|
396
392
|
if not sources:
|
|
397
393
|
self._group_sources.pop(normalized, None)
|
|
398
394
|
self._known_groups.discard(normalized)
|
|
399
|
-
self._ordered_groups = [
|
|
400
|
-
entry
|
|
401
|
-
for entry in self._ordered_groups
|
|
402
|
-
if self._normalize(entry) != normalized
|
|
403
|
-
]
|
|
395
|
+
self._ordered_groups = [entry for entry in self._ordered_groups if self._normalize(entry) != normalized]
|
|
404
396
|
for child in self._group_children.get(normalized, set()):
|
|
405
397
|
self._remove_group_source(child, f"group:{normalized}")
|
|
406
398
|
for skill in self._group_skill_children.get(normalized, set()):
|
|
@@ -418,11 +410,7 @@ class CreationSelection:
|
|
|
418
410
|
self._skill_sources.pop(normalized, None)
|
|
419
411
|
self._known_skills.discard(normalized)
|
|
420
412
|
self._chosen_skills.pop(normalized, None)
|
|
421
|
-
self._skill_order = [
|
|
422
|
-
entry
|
|
423
|
-
for entry in self._skill_order
|
|
424
|
-
if self._normalize(entry) != normalized
|
|
425
|
-
]
|
|
413
|
+
self._skill_order = [entry for entry in self._skill_order if self._normalize(entry) != normalized]
|
|
426
414
|
else:
|
|
427
415
|
self._skill_sources[normalized] = sources
|
|
428
416
|
|
|
@@ -488,11 +476,7 @@ class CreationSelection:
|
|
|
488
476
|
if not sources:
|
|
489
477
|
self._group_sources.pop(normalized, None)
|
|
490
478
|
self._known_groups.discard(normalized)
|
|
491
|
-
self._ordered_groups = [
|
|
492
|
-
entry
|
|
493
|
-
for entry in self._ordered_groups
|
|
494
|
-
if self._normalize(entry) != normalized
|
|
495
|
-
]
|
|
479
|
+
self._ordered_groups = [entry for entry in self._ordered_groups if self._normalize(entry) != normalized]
|
|
496
480
|
else:
|
|
497
481
|
self._group_sources[normalized] = sources
|
|
498
482
|
|
|
@@ -505,9 +489,7 @@ class CreationSelection:
|
|
|
505
489
|
return True
|
|
506
490
|
|
|
507
491
|
def experience_per_level(self) -> int:
|
|
508
|
-
return exp_per_level_for_creation(
|
|
509
|
-
self.race, self.class_type, self.creation_points
|
|
510
|
-
)
|
|
492
|
+
return exp_per_level_for_creation(self.race, self.class_type, self.creation_points)
|
|
511
493
|
|
|
512
494
|
def skill_names(self) -> tuple[str, ...]:
|
|
513
495
|
ordered: list[str] = []
|
|
@@ -599,9 +581,7 @@ def is_valid_account_name(username: str) -> bool:
|
|
|
599
581
|
return False
|
|
600
582
|
|
|
601
583
|
capitalized = candidate.capitalize()
|
|
602
|
-
if capitalized != "Alander" and (
|
|
603
|
-
capitalized.startswith("Alan") or capitalized.endswith("Alander")
|
|
604
|
-
):
|
|
584
|
+
if capitalized != "Alander" and (capitalized.startswith("Alan") or capitalized.endswith("Alander")):
|
|
605
585
|
return False
|
|
606
586
|
|
|
607
587
|
if len(candidate) < 2 or len(candidate) > 12:
|
|
@@ -676,9 +656,7 @@ def _clamp_stats_to_race(stats: Iterable[int], race: PcRaceType) -> list[int]:
|
|
|
676
656
|
return clamped
|
|
677
657
|
|
|
678
658
|
|
|
679
|
-
def finalize_creation_stats(
|
|
680
|
-
race: PcRaceType, class_type: ClassType, stats: Iterable[int]
|
|
681
|
-
) -> list[int]:
|
|
659
|
+
def finalize_creation_stats(race: PcRaceType, class_type: ClassType, stats: Iterable[int]) -> list[int]:
|
|
682
660
|
"""Clamp rolled stats to race bounds and apply the class prime bonus."""
|
|
683
661
|
|
|
684
662
|
clamped = _clamp_stats_to_race(stats, race)
|
|
@@ -859,9 +837,7 @@ def login_with_host(
|
|
|
859
837
|
_mark_account_active(username)
|
|
860
838
|
return LoginResult(account, None, reconnect_requested)
|
|
861
839
|
|
|
862
|
-
failure =
|
|
863
|
-
LoginFailureReason.BAD_CREDENTIALS if exists else LoginFailureReason.UNKNOWN_ACCOUNT
|
|
864
|
-
)
|
|
840
|
+
failure = LoginFailureReason.BAD_CREDENTIALS if exists else LoginFailureReason.UNKNOWN_ACCOUNT
|
|
865
841
|
return LoginResult(None, failure, reconnect_requested)
|
|
866
842
|
|
|
867
843
|
|
|
@@ -936,11 +912,7 @@ def create_character(
|
|
|
936
912
|
sex_value = int(Sex.MALE)
|
|
937
913
|
|
|
938
914
|
hometown = hometown_vnum if hometown_vnum is not None else starting_room_vnum
|
|
939
|
-
weapon_vnum = (
|
|
940
|
-
int(default_weapon_vnum)
|
|
941
|
-
if default_weapon_vnum is not None
|
|
942
|
-
else selected_class.first_weapon_vnum
|
|
943
|
-
)
|
|
915
|
+
weapon_vnum = int(default_weapon_vnum) if default_weapon_vnum is not None else selected_class.first_weapon_vnum
|
|
944
916
|
default_groups = iter_group_names(
|
|
945
917
|
(
|
|
946
918
|
"rom basics",
|
|
@@ -948,30 +920,20 @@ def create_character(
|
|
|
948
920
|
selected_class.default_group,
|
|
949
921
|
)
|
|
950
922
|
)
|
|
951
|
-
groups_tuple = (
|
|
952
|
-
|
|
953
|
-
if creation_groups is not None
|
|
954
|
-
else default_groups
|
|
955
|
-
)
|
|
956
|
-
skills_tuple = (
|
|
957
|
-
iter_skill_names(creation_skills)
|
|
958
|
-
if creation_skills is not None
|
|
959
|
-
else ()
|
|
960
|
-
)
|
|
923
|
+
groups_tuple = iter_group_names(creation_groups) if creation_groups is not None else default_groups
|
|
924
|
+
skills_tuple = iter_skill_names(creation_skills) if creation_skills is not None else ()
|
|
961
925
|
default_points = int(selected_race.points) + (
|
|
962
926
|
_group_cost_for_class(selected_class.default_group, selected_class) or 0
|
|
963
927
|
)
|
|
964
|
-
creation_points_value = (
|
|
965
|
-
int(creation_points)
|
|
966
|
-
if creation_points is not None
|
|
967
|
-
else default_points
|
|
968
|
-
)
|
|
928
|
+
creation_points_value = int(creation_points) if creation_points is not None else default_points
|
|
969
929
|
practice_value = practice if practice is not None else 5
|
|
970
930
|
train_value = train if train is not None else 3
|
|
971
931
|
|
|
972
932
|
session = SessionLocal()
|
|
973
933
|
try:
|
|
974
|
-
|
|
934
|
+
existing = session.query(Character).filter_by(name=sanitized).first()
|
|
935
|
+
if existing:
|
|
936
|
+
print(f"[ERROR] Character creation failed: name '{sanitized}' already exists (id={existing.id})")
|
|
975
937
|
return False
|
|
976
938
|
|
|
977
939
|
new_char = Character(
|
|
@@ -994,6 +956,9 @@ def create_character(
|
|
|
994
956
|
vuln_flags=int(archetype.vulnerability_flags) if archetype else 0,
|
|
995
957
|
practice=int(practice_value),
|
|
996
958
|
train=int(train_value),
|
|
959
|
+
perm_hit=100,
|
|
960
|
+
perm_mana=100,
|
|
961
|
+
perm_move=100,
|
|
997
962
|
act=int(PlayerFlag.NOSUMMON),
|
|
998
963
|
default_weapon_vnum=weapon_vnum,
|
|
999
964
|
newbie_help_seen=False,
|
|
@@ -1004,6 +969,10 @@ def create_character(
|
|
|
1004
969
|
)
|
|
1005
970
|
session.add(new_char)
|
|
1006
971
|
session.commit()
|
|
972
|
+
print(f"[INFO] Character '{sanitized}' created successfully for account {account.username} (id={new_char.id})")
|
|
1007
973
|
return True
|
|
974
|
+
except Exception as e:
|
|
975
|
+
print(f"[ERROR] Failed to create character '{sanitized}': {e}")
|
|
976
|
+
return False
|
|
1008
977
|
finally:
|
|
1009
978
|
session.close()
|
mud/advancement.py
CHANGED
|
@@ -7,7 +7,6 @@ from mud.models.character import Character
|
|
|
7
7
|
from mud.models.constants import LEVEL_HERO
|
|
8
8
|
from mud.models.classes import CLASS_TABLE, ClassType
|
|
9
9
|
from mud.models.races import PcRaceType, list_playable_races
|
|
10
|
-
from mud.persistence import save_character
|
|
11
10
|
from mud.wiznet import WiznetFlag, wiznet
|
|
12
11
|
|
|
13
12
|
BASE_XP_PER_LEVEL = 1000
|
|
@@ -199,4 +198,6 @@ def gain_exp(char: Character, amount: int) -> None:
|
|
|
199
198
|
None,
|
|
200
199
|
0,
|
|
201
200
|
)
|
|
201
|
+
# Lazy import to avoid circular dependency
|
|
202
|
+
from mud.account.account_manager import save_character
|
|
202
203
|
save_character(char)
|
mud/combat/engine.py
CHANGED
|
@@ -33,7 +33,7 @@ from mud.models.constants import (
|
|
|
33
33
|
LEVEL_IMMORTAL,
|
|
34
34
|
WearFlag,
|
|
35
35
|
)
|
|
36
|
-
from mud.
|
|
36
|
+
from mud.account.account_manager import save_character
|
|
37
37
|
from mud.magic import SpellTarget, cold_effect, fire_effect, shock_effect
|
|
38
38
|
from mud.models.social import expand_placeholders
|
|
39
39
|
from mud.utils import rng_mm
|
|
@@ -824,19 +824,11 @@ def _auto_sacrifice(attacker: Character, corpse) -> None:
|
|
|
824
824
|
if silver_reward == 1:
|
|
825
825
|
attacker.send_to_char("Mota gives you one silver coin for your sacrifice.")
|
|
826
826
|
else:
|
|
827
|
-
attacker.send_to_char(
|
|
828
|
-
f"Mota gives you {silver_reward} silver coins for your sacrifice."
|
|
829
|
-
)
|
|
827
|
+
attacker.send_to_char(f"Mota gives you {silver_reward} silver coins for your sacrifice.")
|
|
830
828
|
|
|
831
|
-
corpse_name = (
|
|
832
|
-
getattr(corpse, "short_descr", None)
|
|
833
|
-
or getattr(corpse, "name", "")
|
|
834
|
-
or "corpse"
|
|
835
|
-
)
|
|
829
|
+
corpse_name = getattr(corpse, "short_descr", None) or getattr(corpse, "name", "") or "corpse"
|
|
836
830
|
room.broadcast(
|
|
837
|
-
expand_placeholders(
|
|
838
|
-
"$n sacrifices $N to Mota.", attacker, SimpleNamespace(name=corpse_name)
|
|
839
|
-
),
|
|
831
|
+
expand_placeholders("$n sacrifices $N to Mota.", attacker, SimpleNamespace(name=corpse_name)),
|
|
840
832
|
exclude=attacker,
|
|
841
833
|
)
|
|
842
834
|
wiznet(
|
|
@@ -906,21 +898,15 @@ def _auto_sacrifice(attacker: Character, corpse) -> None:
|
|
|
906
898
|
return
|
|
907
899
|
|
|
908
900
|
attacker.silver = current_silver + share + remainder
|
|
909
|
-
attacker.send_to_char(
|
|
910
|
-
f"You split {silver_reward} silver coins. Your share is {share + remainder} silver."
|
|
911
|
-
)
|
|
901
|
+
attacker.send_to_char(f"You split {silver_reward} silver coins. Your share is {share + remainder} silver.")
|
|
912
902
|
|
|
913
|
-
split_message =
|
|
914
|
-
f"$n splits {silver_reward} silver coins. Your share is {share} silver."
|
|
915
|
-
)
|
|
903
|
+
split_message = f"$n splits {silver_reward} silver coins. Your share is {share} silver."
|
|
916
904
|
for member in group_members:
|
|
917
905
|
if member is attacker:
|
|
918
906
|
continue
|
|
919
907
|
member.silver = max(0, int(getattr(member, "silver", 0) or 0)) + share
|
|
920
908
|
if hasattr(member, "messages"):
|
|
921
|
-
member.messages.append(
|
|
922
|
-
expand_placeholders(split_message, attacker, member)
|
|
923
|
-
)
|
|
909
|
+
member.messages.append(expand_placeholders(split_message, attacker, member))
|
|
924
910
|
|
|
925
911
|
|
|
926
912
|
def _handle_auto_actions(attacker: Character, corpse) -> None:
|
|
@@ -1398,8 +1384,8 @@ def process_weapon_special_attacks(attacker: Character, victim: Character) -> li
|
|
|
1398
1384
|
if wield is None:
|
|
1399
1385
|
return messages
|
|
1400
1386
|
|
|
1401
|
-
|
|
1402
|
-
if
|
|
1387
|
+
current_target = getattr(attacker, "fighting", None)
|
|
1388
|
+
if current_target is not None and current_target is not victim:
|
|
1403
1389
|
return messages
|
|
1404
1390
|
|
|
1405
1391
|
# Get weapon flags - support both extra_flags (for ObjIndex) and weapon_flags attribute
|
|
@@ -1410,11 +1396,7 @@ def process_weapon_special_attacks(attacker: Character, victim: Character) -> li
|
|
|
1410
1396
|
weapon_flags = int(getattr(wield, "extra_flags"))
|
|
1411
1397
|
|
|
1412
1398
|
weapon_level = _weapon_level(wield) or 1
|
|
1413
|
-
weapon_name = (
|
|
1414
|
-
getattr(wield, "name", None)
|
|
1415
|
-
or getattr(wield, "short_descr", None)
|
|
1416
|
-
or "the weapon"
|
|
1417
|
-
)
|
|
1399
|
+
weapon_name = getattr(wield, "name", None) or getattr(wield, "short_descr", None) or "the weapon"
|
|
1418
1400
|
room = getattr(victim, "room", None)
|
|
1419
1401
|
|
|
1420
1402
|
# WEAPON_POISON - ROM src/fight.c L600-634
|
|
@@ -1430,7 +1412,7 @@ def process_weapon_special_attacks(attacker: Character, victim: Character) -> li
|
|
|
1430
1412
|
)
|
|
1431
1413
|
if hasattr(victim, "add_affect"):
|
|
1432
1414
|
victim.add_affect(AffectFlag.POISON)
|
|
1433
|
-
messages.append(
|
|
1415
|
+
messages.append("You feel poison coursing through your veins.")
|
|
1434
1416
|
|
|
1435
1417
|
# WEAPON_VAMPIRIC - ROM src/fight.c L640-649
|
|
1436
1418
|
if weapon_flags & WEAPON_VAMPIRIC:
|
|
@@ -1451,7 +1433,7 @@ def process_weapon_special_attacks(attacker: Character, victim: Character) -> li
|
|
|
1451
1433
|
if hasattr(attacker, "alignment"):
|
|
1452
1434
|
attacker.alignment = max(-1000, attacker.alignment - 1)
|
|
1453
1435
|
|
|
1454
|
-
messages.append(f"{weapon_name}
|
|
1436
|
+
messages.append(f"You feel {weapon_name} drawing your life away.")
|
|
1455
1437
|
|
|
1456
1438
|
# WEAPON_FLAMING - ROM src/fight.c L651-659
|
|
1457
1439
|
if weapon_flags & WEAPON_FLAMING:
|
|
@@ -1461,7 +1443,7 @@ def process_weapon_special_attacks(attacker: Character, victim: Character) -> li
|
|
|
1461
1443
|
room.broadcast(f"{victim.name} is burned by {weapon_name}.", exclude=victim)
|
|
1462
1444
|
fire_effect(victim, weapon_level // 2, dam, SpellTarget.CHAR)
|
|
1463
1445
|
apply_damage(attacker, victim, dam, DamageType.FIRE, show=False)
|
|
1464
|
-
messages.append(f"{weapon_name}
|
|
1446
|
+
messages.append(f"{weapon_name} sears your flesh.")
|
|
1465
1447
|
|
|
1466
1448
|
# WEAPON_FROST - ROM src/fight.c L661-670
|
|
1467
1449
|
if weapon_flags & WEAPON_FROST:
|
|
@@ -1471,7 +1453,7 @@ def process_weapon_special_attacks(attacker: Character, victim: Character) -> li
|
|
|
1471
1453
|
room.broadcast(f"{victim.name} is frozen by {weapon_name}.", exclude=victim)
|
|
1472
1454
|
cold_effect(victim, weapon_level // 2, dam, SpellTarget.CHAR)
|
|
1473
1455
|
apply_damage(attacker, victim, dam, DamageType.COLD, show=False)
|
|
1474
|
-
messages.append(
|
|
1456
|
+
messages.append("The cold touch surrounds you with ice.")
|
|
1475
1457
|
|
|
1476
1458
|
# WEAPON_SHOCKING - ROM src/fight.c L672-681
|
|
1477
1459
|
if weapon_flags & WEAPON_SHOCKING:
|
|
@@ -1484,6 +1466,6 @@ def process_weapon_special_attacks(attacker: Character, victim: Character) -> li
|
|
|
1484
1466
|
)
|
|
1485
1467
|
shock_effect(victim, weapon_level // 2, dam, SpellTarget.CHAR)
|
|
1486
1468
|
apply_damage(attacker, victim, dam, DamageType.LIGHTNING, show=False)
|
|
1487
|
-
messages.append(
|
|
1469
|
+
messages.append("You are shocked by the weapon.")
|
|
1488
1470
|
|
|
1489
1471
|
return messages
|
mud/combat/safety.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Combat safety checks - determine if it's safe to attack.
|
|
3
|
+
|
|
4
|
+
ROM Reference: src/fight.c is_safe
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from mud.models.character import Character
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_safe(char: "Character", victim: "Character") -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Check if it's safe to attack victim (i.e., shouldn't attack).
|
|
17
|
+
|
|
18
|
+
ROM Reference: src/fight.c is_safe (lines 130-230)
|
|
19
|
+
|
|
20
|
+
Returns True if:
|
|
21
|
+
- Victim is in a SAFE room
|
|
22
|
+
- Victim is a shopkeeper
|
|
23
|
+
- Victim is a healer
|
|
24
|
+
- Attacker is too much lower level (for NPCs attacking players)
|
|
25
|
+
"""
|
|
26
|
+
from mud.models.constants import RoomFlag, ActFlag
|
|
27
|
+
|
|
28
|
+
if char is None or victim is None:
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
# Ghost can't fight
|
|
32
|
+
if getattr(char, "is_ghost", False):
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
# Can't fight yourself
|
|
36
|
+
if char is victim:
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
# Check for safe room
|
|
40
|
+
room = getattr(victim, "room", None)
|
|
41
|
+
if room:
|
|
42
|
+
room_flags = getattr(room, "room_flags", 0)
|
|
43
|
+
if room_flags & RoomFlag.ROOM_SAFE:
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
# Check if victim is a shopkeeper or healer
|
|
47
|
+
victim_act = getattr(victim, "act", 0)
|
|
48
|
+
if getattr(victim, "is_npc", False):
|
|
49
|
+
# Check for special mob types that shouldn't be attacked
|
|
50
|
+
if victim_act & ActFlag.IS_HEALER:
|
|
51
|
+
return True
|
|
52
|
+
if victim_act & ActFlag.IS_CHANGER:
|
|
53
|
+
return True
|
|
54
|
+
if victim_act & ActFlag.TRAIN:
|
|
55
|
+
return True
|
|
56
|
+
if victim_act & ActFlag.PRACTICE:
|
|
57
|
+
return True
|
|
58
|
+
if victim_act & ActFlag.GAIN:
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
# Check shop - if mob has a shop, it's a shopkeeper
|
|
62
|
+
if hasattr(victim, "pShop") and getattr(victim, "pShop", None):
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
# NPC attacking much lower level player
|
|
66
|
+
if getattr(char, "is_npc", False) and not getattr(victim, "is_npc", True):
|
|
67
|
+
char_level = getattr(char, "level", 1)
|
|
68
|
+
victim_level = getattr(victim, "level", 1)
|
|
69
|
+
if victim_level < char_level - 10:
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_safe_spell(char: "Character", victim: "Character", area: bool = False) -> bool:
|
|
76
|
+
"""
|
|
77
|
+
Check if it's safe to cast an offensive spell on victim.
|
|
78
|
+
|
|
79
|
+
ROM Reference: src/fight.c is_safe_spell
|
|
80
|
+
"""
|
|
81
|
+
# Can't spell yourself offensively in most cases
|
|
82
|
+
if char is victim and not area:
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
# Use same logic as regular combat safety
|
|
86
|
+
return is_safe(char, victim)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check_killer(char: "Character", victim: "Character") -> None:
|
|
90
|
+
"""
|
|
91
|
+
Mark character as a killer if they attack an innocent.
|
|
92
|
+
|
|
93
|
+
ROM Reference: src/fight.c check_killer
|
|
94
|
+
"""
|
|
95
|
+
from mud.models.constants import PlayerFlag
|
|
96
|
+
|
|
97
|
+
# Only applies to players
|
|
98
|
+
if getattr(char, "is_npc", True):
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# NPCs don't make you a killer
|
|
102
|
+
if getattr(victim, "is_npc", True):
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# Set KILLER flag
|
|
106
|
+
char.act = getattr(char, "act", 0) | PlayerFlag.KILLER
|
mud/commands/admin_commands.py
CHANGED
|
@@ -12,7 +12,7 @@ from mud.config import (
|
|
|
12
12
|
from mud.models.character import Character, character_registry
|
|
13
13
|
from mud.models.constants import CommFlag, PlayerFlag, Sex
|
|
14
14
|
from mud.net.session import SESSIONS
|
|
15
|
-
from mud.
|
|
15
|
+
from mud.account.account_manager import save_character as save_player_file
|
|
16
16
|
from mud.registry import room_registry
|
|
17
17
|
from mud.security import bans
|
|
18
18
|
from mud.security.bans import BanFlag, BanPermissionError
|
mud/commands/affects.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Affects command - show active spell/affect effects on character.
|
|
3
|
+
|
|
4
|
+
ROM Reference: src/act_info.c do_affects (lines 2300-2400)
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from mud.models.character import Character
|
|
9
|
+
from mud.models.constants import AffectFlag
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Mapping of affect flags to human-readable names
|
|
13
|
+
_AFFECT_NAMES = {
|
|
14
|
+
AffectFlag.BLIND: "blindness",
|
|
15
|
+
AffectFlag.INVISIBLE: "invisibility",
|
|
16
|
+
AffectFlag.DETECT_EVIL: "detect evil",
|
|
17
|
+
AffectFlag.DETECT_INVIS: "detect invisibility",
|
|
18
|
+
AffectFlag.DETECT_MAGIC: "detect magic",
|
|
19
|
+
AffectFlag.DETECT_HIDDEN: "detect hidden",
|
|
20
|
+
AffectFlag.DETECT_GOOD: "detect good",
|
|
21
|
+
AffectFlag.SANCTUARY: "sanctuary",
|
|
22
|
+
AffectFlag.FAERIE_FIRE: "faerie fire",
|
|
23
|
+
AffectFlag.INFRARED: "infrared vision",
|
|
24
|
+
AffectFlag.CURSE: "curse",
|
|
25
|
+
AffectFlag.POISON: "poison",
|
|
26
|
+
AffectFlag.PROTECT_EVIL: "protection evil",
|
|
27
|
+
AffectFlag.PROTECT_GOOD: "protection good",
|
|
28
|
+
AffectFlag.SNEAK: "sneak",
|
|
29
|
+
AffectFlag.HIDE: "hide",
|
|
30
|
+
AffectFlag.SLEEP: "sleep",
|
|
31
|
+
AffectFlag.CHARM: "charm",
|
|
32
|
+
AffectFlag.FLYING: "fly",
|
|
33
|
+
AffectFlag.PASS_DOOR: "pass door",
|
|
34
|
+
AffectFlag.HASTE: "haste",
|
|
35
|
+
AffectFlag.CALM: "calm",
|
|
36
|
+
AffectFlag.PLAGUE: "plague",
|
|
37
|
+
AffectFlag.WEAKEN: "weaken",
|
|
38
|
+
AffectFlag.DARK_VISION: "dark vision",
|
|
39
|
+
AffectFlag.BERSERK: "berserk",
|
|
40
|
+
AffectFlag.SWIM: "swim",
|
|
41
|
+
AffectFlag.REGENERATION: "regeneration",
|
|
42
|
+
AffectFlag.SLOW: "slow",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def do_affects(char: Character, args: str) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Display active affects on the character.
|
|
49
|
+
|
|
50
|
+
ROM Reference: src/act_info.c do_affects (lines 2300-2400)
|
|
51
|
+
|
|
52
|
+
Usage: affects
|
|
53
|
+
"""
|
|
54
|
+
lines = []
|
|
55
|
+
|
|
56
|
+
# Check spell_effects (detailed spell effects with duration)
|
|
57
|
+
spell_effects = getattr(char, "spell_effects", {})
|
|
58
|
+
if spell_effects:
|
|
59
|
+
lines.append("You are affected by the following spells:")
|
|
60
|
+
for spell_name, effect in spell_effects.items():
|
|
61
|
+
duration = getattr(effect, "duration", -1)
|
|
62
|
+
if duration < 0:
|
|
63
|
+
lines.append(f" {spell_name}: permanent")
|
|
64
|
+
else:
|
|
65
|
+
lines.append(f" {spell_name}: {duration} hours remaining")
|
|
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:")
|
|
72
|
+
else:
|
|
73
|
+
lines.append("\nYou also have these affects:")
|
|
74
|
+
|
|
75
|
+
for flag, name in _AFFECT_NAMES.items():
|
|
76
|
+
if affected_by & flag:
|
|
77
|
+
# Check if this isn't already shown in spell_effects
|
|
78
|
+
if name not in spell_effects:
|
|
79
|
+
lines.append(f" {name}")
|
|
80
|
+
|
|
81
|
+
# Check for special conditions
|
|
82
|
+
if getattr(char, "position", 0) == 0: # DEAD
|
|
83
|
+
lines.append("You are DEAD.")
|
|
84
|
+
|
|
85
|
+
# Check for hunger/thirst (if applicable)
|
|
86
|
+
pcdata = getattr(char, "pcdata", None)
|
|
87
|
+
if pcdata:
|
|
88
|
+
hunger = getattr(pcdata, "condition", [0, 0, 0, 0])
|
|
89
|
+
if len(hunger) >= 2:
|
|
90
|
+
if hunger[0] == 0: # COND_FULL
|
|
91
|
+
lines.append("You are hungry.")
|
|
92
|
+
if hunger[1] == 0: # COND_THIRST
|
|
93
|
+
lines.append("You are thirsty.")
|
|
94
|
+
|
|
95
|
+
if not lines:
|
|
96
|
+
return "You are not affected by any spells."
|
|
97
|
+
|
|
98
|
+
return "\n".join(lines)
|