rom24-quickmud-python 2.13.31__py3-none-any.whl → 2.14.264__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mud/account/__init__.py +2 -0
- mud/account/account_manager.py +36 -0
- mud/account/account_service.py +13 -0
- mud/ai/__init__.py +36 -19
- mud/ai/aggressive.py +19 -6
- mud/characters/follow.py +13 -17
- mud/combat/assist.py +33 -10
- mud/combat/death.py +183 -89
- mud/combat/engine.py +161 -127
- mud/combat/messages.py +4 -3
- mud/combat/safety.py +181 -52
- mud/commands/advancement.py +27 -5
- mud/commands/affects.py +5 -2
- mud/commands/build.py +19 -26
- mud/commands/character.py +5 -2
- mud/commands/combat.py +371 -151
- mud/commands/communication.py +122 -60
- mud/commands/compare.py +29 -12
- mud/commands/consider.py +19 -5
- mud/commands/consumption.py +44 -47
- mud/commands/dispatcher.py +97 -64
- mud/commands/doors.py +38 -14
- mud/commands/equipment.py +69 -70
- mud/commands/give.py +32 -22
- mud/commands/group_commands.py +98 -125
- mud/commands/healer.py +29 -3
- mud/commands/imm_commands.py +48 -67
- mud/commands/imm_emote.py +6 -10
- mud/commands/imm_load.py +16 -17
- mud/commands/imm_search.py +124 -74
- mud/commands/imm_server.py +45 -25
- mud/commands/imm_set.py +13 -18
- mud/commands/info.py +11 -15
- mud/commands/info_extended.py +29 -11
- mud/commands/inspection.py +6 -5
- mud/commands/inventory.py +68 -23
- mud/commands/magic_items.py +15 -7
- mud/commands/misc_info.py +6 -14
- mud/commands/misc_player.py +22 -17
- mud/commands/murder.py +50 -7
- mud/commands/obj_manipulation.py +32 -8
- mud/commands/player_config.py +33 -52
- mud/commands/remaining_rom.py +256 -73
- mud/commands/session.py +65 -71
- mud/commands/shop.py +74 -24
- mud/commands/socials.py +7 -0
- mud/commands/thief_skills.py +16 -8
- mud/game_loop.py +321 -127
- mud/groups/xp.py +7 -8
- mud/handler.py +61 -10
- mud/loaders/json_loader.py +6 -12
- mud/loaders/shop_loader.py +10 -1
- mud/magic/effects.py +69 -19
- mud/mob_cmds.py +28 -20
- mud/mobprog.py +32 -8
- mud/models/character.py +74 -22
- mud/models/room.py +2 -1
- mud/models/skill.py +9 -0
- mud/net/connection.py +327 -191
- mud/net/protocol.py +19 -3
- mud/net/session.py +1 -0
- mud/registry.py +4 -0
- mud/scripts/convert_skills_to_json.py +5 -0
- mud/skills/handlers.py +472 -482
- mud/skills/metadata.py +45 -0
- mud/skills/registry.py +30 -33
- mud/skills/skill_lookup.py +148 -0
- mud/spawning/reset_handler.py +46 -32
- mud/spawning/templates.py +9 -4
- mud/spec_funs.py +81 -36
- mud/utils/act.py +7 -0
- mud/utils/messaging.py +66 -0
- mud/world/char_find.py +6 -1
- mud/world/look.py +133 -52
- mud/world/movement.py +18 -4
- mud/world/vision.py +14 -2
- mud/world/world_state.py +10 -1
- {rom24_quickmud_python-2.13.31.dist-info → rom24_quickmud_python-2.14.264.dist-info}/METADATA +30 -19
- {rom24_quickmud_python-2.13.31.dist-info → rom24_quickmud_python-2.14.264.dist-info}/RECORD +83 -82
- {rom24_quickmud_python-2.13.31.dist-info → rom24_quickmud_python-2.14.264.dist-info}/WHEEL +1 -1
- {rom24_quickmud_python-2.13.31.dist-info → rom24_quickmud_python-2.14.264.dist-info}/entry_points.txt +0 -0
- {rom24_quickmud_python-2.13.31.dist-info → rom24_quickmud_python-2.14.264.dist-info}/licenses/LICENSE +0 -0
- {rom24_quickmud_python-2.13.31.dist-info → rom24_quickmud_python-2.14.264.dist-info}/top_level.txt +0 -0
mud/account/__init__.py
CHANGED
|
@@ -32,6 +32,7 @@ from .account_service import (
|
|
|
32
32
|
lookup_creation_race,
|
|
33
33
|
lookup_hometown,
|
|
34
34
|
lookup_weapon_choice,
|
|
35
|
+
mark_character_active,
|
|
35
36
|
release_account,
|
|
36
37
|
release_character,
|
|
37
38
|
roll_creation_stats,
|
|
@@ -67,6 +68,7 @@ __all__ = [
|
|
|
67
68
|
"sanitize_account_name",
|
|
68
69
|
"clear_active_accounts",
|
|
69
70
|
"is_account_active",
|
|
71
|
+
"mark_character_active",
|
|
70
72
|
"release_account",
|
|
71
73
|
"release_character",
|
|
72
74
|
"LoginFailureReason",
|
mud/account/account_manager.py
CHANGED
|
@@ -76,6 +76,42 @@ def load_character(char_name: str, _ignored: str | None = None) -> Character | N
|
|
|
76
76
|
session.close()
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
def delete_character(char_name: str) -> bool:
|
|
80
|
+
"""Permanently delete a character from the canonical store (DB row).
|
|
81
|
+
|
|
82
|
+
DB-canonical (INV-008): the player's gameplay state and auth both live in
|
|
83
|
+
the ``characters`` DB row, so deletion means removing that row — NOT
|
|
84
|
+
unlinking a pfile (there is no JSON pfile path). Also drops any matching
|
|
85
|
+
runtime ``Character`` from ``character_registry`` so the name cannot be
|
|
86
|
+
referenced after deletion (INV-003 complement).
|
|
87
|
+
|
|
88
|
+
ROM Reference: src/act_comm.c do_delete (lines 54-93) — ROM ``do_quit``s the
|
|
89
|
+
character (which saves the pfile) and then ``unlink(strsave)`` removes it.
|
|
90
|
+
The DB-row delete here is the faithful equivalent of that ``unlink``.
|
|
91
|
+
|
|
92
|
+
Returns True if a row was deleted, False if no row existed.
|
|
93
|
+
"""
|
|
94
|
+
# Drop any live runtime instance(s) by name first (idempotent).
|
|
95
|
+
for prior in [c for c in character_registry if getattr(c, "name", None) == char_name]:
|
|
96
|
+
character_registry.remove(prior)
|
|
97
|
+
|
|
98
|
+
session = None
|
|
99
|
+
try:
|
|
100
|
+
session = SessionLocal()
|
|
101
|
+
db_char = session.query(DBCharacter).filter(DBCharacter.name == char_name).first()
|
|
102
|
+
if db_char is None:
|
|
103
|
+
return False
|
|
104
|
+
session.delete(db_char)
|
|
105
|
+
session.commit()
|
|
106
|
+
return True
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print(f"[ERROR] delete_character failed for {char_name}: {e}")
|
|
109
|
+
return False
|
|
110
|
+
finally:
|
|
111
|
+
if session:
|
|
112
|
+
session.close()
|
|
113
|
+
|
|
114
|
+
|
|
79
115
|
def save_character(character: Character) -> None:
|
|
80
116
|
"""Persist ``character`` to the DB row (DB-canonical path).
|
|
81
117
|
|
mud/account/account_service.py
CHANGED
|
@@ -824,6 +824,19 @@ def _mark_character_active(name: str) -> None:
|
|
|
824
824
|
_active_characters.add(_normalize(name))
|
|
825
825
|
|
|
826
826
|
|
|
827
|
+
def mark_character_active(name: str) -> None:
|
|
828
|
+
"""Set the active-session marker for this character.
|
|
829
|
+
|
|
830
|
+
Public counterpart to :func:`release_character`. ``login_with_host`` marks
|
|
831
|
+
returning logins internally; the INV-051 deferred-persistence new-character
|
|
832
|
+
path needs this entry point because it no longer routes through
|
|
833
|
+
``login_with_host`` (it persists via ``create_character`` at creation end),
|
|
834
|
+
so a freshly-created in-game character must still be flagged active for the
|
|
835
|
+
name-phase duplicate-login check (``connection.py`` ``is_account_active``).
|
|
836
|
+
"""
|
|
837
|
+
_mark_character_active(name)
|
|
838
|
+
|
|
839
|
+
|
|
827
840
|
def release_character(name: str) -> None:
|
|
828
841
|
"""Clear the active-session marker for this character."""
|
|
829
842
|
_active_characters.discard(_normalize(name))
|
mud/ai/__init__.py
CHANGED
|
@@ -100,18 +100,6 @@ def _find_character(name: str) -> Character | None:
|
|
|
100
100
|
return None
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
def _broadcast_room(room: Room, message: str, exclude: object | None = None) -> None:
|
|
104
|
-
if hasattr(room, "broadcast"):
|
|
105
|
-
room.broadcast(message, exclude=exclude)
|
|
106
|
-
return
|
|
107
|
-
for occupant in getattr(room, "people", []) or []:
|
|
108
|
-
if occupant is exclude:
|
|
109
|
-
continue
|
|
110
|
-
messages = getattr(occupant, "messages", None)
|
|
111
|
-
if isinstance(messages, list):
|
|
112
|
-
messages.append(message)
|
|
113
|
-
|
|
114
|
-
|
|
115
103
|
def _can_loot(mob: Character, obj: Object) -> bool:
|
|
116
104
|
if getattr(mob, "is_admin", False):
|
|
117
105
|
return True
|
|
@@ -187,11 +175,13 @@ def _take_object(mob: Character, obj: Object) -> None:
|
|
|
187
175
|
if isinstance(inventory, list) and obj not in inventory:
|
|
188
176
|
inventory.insert(0, obj)
|
|
189
177
|
|
|
178
|
+
# mirroring ROM src/update.c:491 — act("$n gets $p.", ch, obj_best, NULL, TO_ROOM)
|
|
179
|
+
# act_to_room handles per-recipient formatting + TRIG_ACT dispatch (src/comm.c:2384).
|
|
190
180
|
room = getattr(mob, "room", None)
|
|
191
|
-
if room is not None
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
room
|
|
181
|
+
if room is not None:
|
|
182
|
+
from mud.utils.act import act_to_room
|
|
183
|
+
|
|
184
|
+
act_to_room(room, "$n gets $p.", mob, arg1=obj, exclude=mob)
|
|
195
185
|
|
|
196
186
|
|
|
197
187
|
def _maybe_scavenge(mob: Character, room: Room) -> None:
|
|
@@ -239,8 +229,15 @@ def _maybe_wander(mob: Character, room: Room) -> None:
|
|
|
239
229
|
if rng_mm.number_bits(3) != 0:
|
|
240
230
|
return
|
|
241
231
|
|
|
242
|
-
# ROM
|
|
243
|
-
door =
|
|
232
|
+
# mirroring ROM src/update.c:498 — wander draws a SINGLE 5-bit direction
|
|
233
|
+
# `(door = number_bits(5)) <= 5` and aborts the whole wander when the roll
|
|
234
|
+
# exceeds 5 (so a mob only wanders on 6/32 of otherwise-eligible ticks).
|
|
235
|
+
# NOT number_door() (the do_flee/do_mpflee primitive, src/db.c:3541), which
|
|
236
|
+
# re-rolls with a 3-bit mask until ≤5 and therefore always returns a valid
|
|
237
|
+
# door — that over-wanders ~5× and desyncs the shared Mitchell-Moore stream.
|
|
238
|
+
door = rng_mm.number_bits(5)
|
|
239
|
+
if door > 5:
|
|
240
|
+
return
|
|
244
241
|
|
|
245
242
|
exit_obj = _valid_exit(room, door)
|
|
246
243
|
if exit_obj is None:
|
|
@@ -275,7 +272,16 @@ def _maybe_wander(mob: Character, room: Room) -> None:
|
|
|
275
272
|
def mobile_update() -> None:
|
|
276
273
|
"""Mirror ROM ``mobile_update`` scavenging, wandering, and mobprog triggers."""
|
|
277
274
|
|
|
278
|
-
|
|
275
|
+
# mirroring ROM src/update.c:416 — mobile_update walks ``char_list``, which
|
|
276
|
+
# src/db.c:2256-2257 (create_mobile) head-inserts, so ROM visits the NEWEST
|
|
277
|
+
# mob first. ``character_registry`` is append-order, so iterate it reversed
|
|
278
|
+
# — load-bearing for the shared RNG draw order (shop-wealth rolls, scavenger
|
|
279
|
+
# 1/64, wander door rolls), like violence_tick/char_update/obj_update. GL-042.
|
|
280
|
+
for mob in list(reversed(character_registry)):
|
|
281
|
+
# mirroring ROM extract_char re-linking — a char removed from char_list
|
|
282
|
+
# mid-tick is never revisited in ROM's walk; skip extracted mobs. GL-042.
|
|
283
|
+
if mob not in character_registry:
|
|
284
|
+
continue
|
|
279
285
|
if not getattr(mob, "is_npc", False):
|
|
280
286
|
continue
|
|
281
287
|
room = getattr(mob, "room", None)
|
|
@@ -289,6 +295,17 @@ def mobile_update() -> None:
|
|
|
289
295
|
if not _mob_has_act_flag(mob, ActFlag.UPDATE_ALWAYS):
|
|
290
296
|
continue
|
|
291
297
|
|
|
298
|
+
# mirroring ROM src/update.c:425-431 — the special procedure runs INSIDE
|
|
299
|
+
# the per-mob loop, after the charm/empty gates and BEFORE shop-gold,
|
|
300
|
+
# triggers, scavenge, and wander. A TRUE result `continue`s, skipping the
|
|
301
|
+
# rest of this mob's tick. Dispatched here (not as a separate
|
|
302
|
+
# run_npc_specs pass) so the gates and suppression match ROM exactly
|
|
303
|
+
# and the spec's RNG draws interleave per-mob with scavenge/wander. INV-049.
|
|
304
|
+
from mud.spec_funs import run_spec_fun
|
|
305
|
+
|
|
306
|
+
if run_spec_fun(mob):
|
|
307
|
+
continue
|
|
308
|
+
|
|
292
309
|
shop, wealth = _resolve_shop_info(mob)
|
|
293
310
|
if shop is not None and wealth > 0:
|
|
294
311
|
try:
|
mud/ai/aggressive.py
CHANGED
|
@@ -54,7 +54,17 @@ def _eligible_victims(ch: Character, occupants: Iterable[Character]) -> Iterable
|
|
|
54
54
|
def aggressive_update() -> None:
|
|
55
55
|
"""Wake aggressive NPCs and initiate combat when players enter their rooms."""
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
# mirroring ROM src/update.c:1087 — aggr_update walks ``char_list`` for the
|
|
58
|
+
# PC watcher ``wch``; src/db.c:2256-2257 / src/nanny.c:757-758 head-insert,
|
|
59
|
+
# so ROM visits the NEWEST character first. ``character_registry`` is
|
|
60
|
+
# append-order, so iterate it reversed — load-bearing for the shared RNG
|
|
61
|
+
# draw order (number_bits(1) aggression coin, number_range victim
|
|
62
|
+
# reservoir), like violence_tick/char_update/mobile_update. GL-043.
|
|
63
|
+
for watcher in reversed(list(character_registry)):
|
|
64
|
+
# mirroring ROM's saved ``wch_next`` pointer — a char extracted
|
|
65
|
+
# mid-tick (multi_hit kill) is never revisited in ROM's walk. GL-043.
|
|
66
|
+
if watcher not in character_registry:
|
|
67
|
+
continue
|
|
58
68
|
if getattr(watcher, "is_npc", False):
|
|
59
69
|
continue
|
|
60
70
|
if watcher.is_immortal():
|
|
@@ -98,9 +108,12 @@ def aggressive_update() -> None:
|
|
|
98
108
|
if victim is None:
|
|
99
109
|
continue
|
|
100
110
|
|
|
111
|
+
# mirroring ROM src/update.c:1136 — aggr_update ends each aggression
|
|
112
|
+
# with a bare multi_hit(ch, victim, TYPE_UNDEFINED). It does NOT call
|
|
113
|
+
# check_assist: auto-assist is exclusively violence_update's job
|
|
114
|
+
# (src/fight.c:90, run on the next PULSE_VIOLENCE — Python's
|
|
115
|
+
# game_loop.violence_tick). Calling check_assist here started assists
|
|
116
|
+
# a full tick early and drew extra coins from the shared MM RNG
|
|
117
|
+
# stream (the comment that used to cite "fight.c:90" mislabeled the
|
|
118
|
+
# violence_update site as if it belonged to aggr_update).
|
|
101
119
|
multi_hit(mob, victim)
|
|
102
|
-
|
|
103
|
-
# ROM src/fight.c:90 - Check for assist after combat starts
|
|
104
|
-
from mud.combat.assist import check_assist
|
|
105
|
-
|
|
106
|
-
check_assist(mob, victim)
|
mud/characters/follow.py
CHANGED
|
@@ -3,24 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from mud.models.constants import AffectFlag
|
|
6
|
+
from mud.utils.act import act_format
|
|
6
7
|
from mud.utils.messaging import push_message
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING: # pragma: no cover - import for type checkers only
|
|
9
10
|
from mud.models.character import Character
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def _display_name(character: Character | None) -> str:
|
|
13
|
-
if character is None:
|
|
14
|
-
return "Someone"
|
|
15
|
-
name = getattr(character, "name", None)
|
|
16
|
-
if isinstance(name, str) and name:
|
|
17
|
-
return name
|
|
18
|
-
short_descr = getattr(character, "short_descr", None)
|
|
19
|
-
if isinstance(short_descr, str) and short_descr:
|
|
20
|
-
return short_descr
|
|
21
|
-
return "Someone"
|
|
22
|
-
|
|
23
|
-
|
|
24
13
|
def add_follower(follower: Character, master: Character) -> None:
|
|
25
14
|
"""Attach ``follower`` to ``master`` mirroring ROM ``add_follower``."""
|
|
26
15
|
# mirroring ROM src/act_comm.c:1591-1607
|
|
@@ -38,10 +27,13 @@ def add_follower(follower: Character, master: Character) -> None:
|
|
|
38
27
|
if can_see_character(master, follower):
|
|
39
28
|
# mirroring ROM src/act_comm.c:1602-1603 — act(..., TO_VICT)
|
|
40
29
|
# writes immediately to the descriptor; mailbox is fallback only.
|
|
41
|
-
|
|
30
|
+
# FOLLOW-003: ROM act("$n now follows you.", ch, NULL, master, TO_VICT) —
|
|
31
|
+
# $n = PERS(follower, master) = NPC short_descr (cap), not the keyword name.
|
|
32
|
+
push_message(master, act_format("$n now follows you.", recipient=master, actor=follower))
|
|
42
33
|
|
|
43
|
-
# ROM line 1605: TO_CHAR is unconditional.
|
|
44
|
-
|
|
34
|
+
# ROM line 1605: TO_CHAR is unconditional. FOLLOW-004: $N = PERS(master, ch)
|
|
35
|
+
# is gated on can_see(ch, master) — an unseen master renders "someone".
|
|
36
|
+
push_message(follower, act_format("You now follow $N.", recipient=follower, arg2=master))
|
|
45
37
|
|
|
46
38
|
|
|
47
39
|
def stop_follower(follower: Character) -> None:
|
|
@@ -65,8 +57,12 @@ def stop_follower(follower: Character) -> None:
|
|
|
65
57
|
# immediately to the descriptor; push_message routes a connected PC to the
|
|
66
58
|
# async send and falls back to the mailbox only for disconnected chars
|
|
67
59
|
# (matching add_follower above — ACT_COMM-003 / INV-001 wrong-channel).
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
# FOLLOW-003: ROM act("$n stops following you.", ch, NULL, ch->master,
|
|
61
|
+
# TO_VICT) — $n = PERS(follower, master) = NPC short_descr (cap).
|
|
62
|
+
push_message(master, act_format("$n stops following you.", recipient=master, actor=follower))
|
|
63
|
+
# FOLLOW-004: $N = PERS(master, ch) — gated on can_see(ch, master); an
|
|
64
|
+
# unseen master (e.g. blind follower) renders "someone".
|
|
65
|
+
push_message(follower, act_format("You stop following $N.", recipient=follower, arg2=master))
|
|
70
66
|
|
|
71
67
|
if getattr(master, "pet", None) is follower:
|
|
72
68
|
master.pet = None
|
mud/combat/assist.py
CHANGED
|
@@ -10,7 +10,6 @@ from typing import TYPE_CHECKING
|
|
|
10
10
|
|
|
11
11
|
from mud.characters import is_same_group
|
|
12
12
|
from mud.combat.engine import is_evil, is_good, is_neutral, multi_hit
|
|
13
|
-
from mud.combat.safety import is_safe
|
|
14
13
|
from mud.models.constants import AffectFlag, OffFlag, PlayerFlag
|
|
15
14
|
from mud.utils import rng_mm
|
|
16
15
|
from mud.world.vision import can_see_character
|
|
@@ -81,8 +80,23 @@ def check_assist(ch: Character, victim: Character) -> None:
|
|
|
81
80
|
is_rch_charmed = _is_affected(rch, AffectFlag.CHARM)
|
|
82
81
|
|
|
83
82
|
if (is_rch_autoassist or is_rch_charmed) and is_same_group(ch, rch):
|
|
84
|
-
|
|
83
|
+
# INV-050: route through the faithful ROM is_safe() mirror
|
|
84
|
+
# (_kill_safety_message, src/fight.c:1018-1124) rather than the
|
|
85
|
+
# silent bool combat.safety.is_safe. ROM check_assist gates on
|
|
86
|
+
# `!is_safe(rch, victim)` (src/fight.c:131); is_safe writes its
|
|
87
|
+
# OWN rejection line to rch via send_to_char BEFORE returning
|
|
88
|
+
# TRUE (e.g. "Join a clan if you want to kill players." for a
|
|
89
|
+
# non-clan PC autoassister) — so a blocked assister sees the
|
|
90
|
+
# line. A None return == not safe → rch assists. The silent bool
|
|
91
|
+
# lacked the PC clan ladder, so non-clan PCs wrongly auto-assisted
|
|
92
|
+
# in PvP. (Function-local import avoids an engine→command cycle.)
|
|
93
|
+
from mud.commands.combat import _kill_safety_message
|
|
94
|
+
|
|
95
|
+
safety_message = _kill_safety_message(rch, victim)
|
|
96
|
+
if safety_message is None:
|
|
85
97
|
multi_hit(rch, victim, None)
|
|
98
|
+
else:
|
|
99
|
+
_send_to_char(rch, safety_message + "\n")
|
|
86
100
|
|
|
87
101
|
continue
|
|
88
102
|
|
|
@@ -95,19 +109,25 @@ def check_assist(ch: Character, victim: Character) -> None:
|
|
|
95
109
|
rch_off_flags = getattr(rch, "off_flags", 0)
|
|
96
110
|
should_assist = False
|
|
97
111
|
|
|
112
|
+
# mirroring ROM src/fight.c:141-150 — all five assist conditions are a
|
|
113
|
+
# flat || OR chain; C evaluates each until one is true. Python's
|
|
114
|
+
# `elif` would enter the ASSIST_RACE branch (flag set) and skip
|
|
115
|
+
# ASSIST_ALIGN/ASSIST_VNUM even when the inner race predicate fails.
|
|
116
|
+
# Independent `if` checks restore the ROM || semantics.
|
|
117
|
+
|
|
98
118
|
# ASSIST_ALL: Assist any mob
|
|
99
119
|
if rch_off_flags & OffFlag.ASSIST_ALL:
|
|
100
120
|
should_assist = True
|
|
101
121
|
|
|
102
|
-
# ASSIST_RACE: Assist same race
|
|
103
|
-
|
|
122
|
+
# ASSIST_RACE: Assist same race (ROM src/fight.c:143-144)
|
|
123
|
+
if rch_off_flags & OffFlag.ASSIST_RACE:
|
|
104
124
|
ch_race = getattr(ch, "race", None)
|
|
105
125
|
rch_race = getattr(rch, "race", None)
|
|
106
126
|
if ch_race and rch_race and ch_race == rch_race:
|
|
107
127
|
should_assist = True
|
|
108
128
|
|
|
109
|
-
# ASSIST_ALIGN: Assist same alignment
|
|
110
|
-
|
|
129
|
+
# ASSIST_ALIGN: Assist same alignment (ROM src/fight.c:145-148)
|
|
130
|
+
if rch_off_flags & OffFlag.ASSIST_ALIGN:
|
|
111
131
|
if (
|
|
112
132
|
(is_good(rch) and is_good(ch))
|
|
113
133
|
or (is_evil(rch) and is_evil(ch))
|
|
@@ -115,8 +135,8 @@ def check_assist(ch: Character, victim: Character) -> None:
|
|
|
115
135
|
):
|
|
116
136
|
should_assist = True
|
|
117
137
|
|
|
118
|
-
# ASSIST_VNUM: Assist same vnum (
|
|
119
|
-
|
|
138
|
+
# ASSIST_VNUM: Assist same vnum/mob prototype (ROM src/fight.c:149-150)
|
|
139
|
+
if rch_off_flags & OffFlag.ASSIST_VNUM:
|
|
120
140
|
ch_vnum = getattr(ch, "vnum", None)
|
|
121
141
|
rch_vnum = getattr(rch, "vnum", None)
|
|
122
142
|
if ch_vnum is not None and rch_vnum is not None and ch_vnum == rch_vnum:
|
|
@@ -137,12 +157,15 @@ def check_assist(ch: Character, victim: Character) -> None:
|
|
|
137
157
|
target = None
|
|
138
158
|
number = 0
|
|
139
159
|
|
|
140
|
-
#
|
|
160
|
+
# ROM src/fight.c:159-170 — number++ is INSIDE the selection block;
|
|
161
|
+
# it only increments when a new target is chosen, not on every
|
|
162
|
+
# visible group member. This matches ROM's biased selection distribution
|
|
163
|
+
# and, critically, keeps the shared MM RNG stream in sync.
|
|
141
164
|
for vch in list(people_in_room):
|
|
142
165
|
if can_see_character(rch, vch) and is_same_group(vch, victim):
|
|
143
166
|
if rng_mm.number_range(0, number) == 0:
|
|
144
167
|
target = vch
|
|
145
|
-
|
|
168
|
+
number += 1 # mirroring ROM src/fight.c:165
|
|
146
169
|
|
|
147
170
|
# ROM lines 172-176: Attack the selected target
|
|
148
171
|
if target is not None:
|