rom24-quickmud-python 2.14.211__py3-none-any.whl → 2.14.268__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/admin_logging/admin.py +3 -3
- mud/combat/engine.py +78 -23
- mud/commands/advancement.py +6 -1
- mud/commands/affects.py +5 -2
- mud/commands/combat.py +92 -29
- mud/commands/consumption.py +4 -28
- mud/commands/doors.py +17 -7
- mud/commands/equipment.py +5 -23
- mud/commands/inventory.py +62 -22
- mud/commands/obj_manipulation.py +25 -2
- mud/commands/shop.py +11 -4
- mud/game_loop.py +66 -34
- mud/mobprog.py +11 -1
- mud/models/character.py +19 -0
- mud/net/connection.py +33 -16
- mud/net/session.py +1 -0
- mud/skills/handlers.py +148 -206
- mud/skills/registry.py +4 -10
- mud/skills/skill_lookup.py +148 -0
- mud/world/look.py +10 -4
- mud/world/vision.py +4 -2
- {rom24_quickmud_python-2.14.211.dist-info → rom24_quickmud_python-2.14.268.dist-info}/METADATA +7 -7
- {rom24_quickmud_python-2.14.211.dist-info → rom24_quickmud_python-2.14.268.dist-info}/RECORD +27 -26
- {rom24_quickmud_python-2.14.211.dist-info → rom24_quickmud_python-2.14.268.dist-info}/WHEEL +1 -1
- {rom24_quickmud_python-2.14.211.dist-info → rom24_quickmud_python-2.14.268.dist-info}/entry_points.txt +0 -0
- {rom24_quickmud_python-2.14.211.dist-info → rom24_quickmud_python-2.14.268.dist-info}/licenses/LICENSE +0 -0
- {rom24_quickmud_python-2.14.211.dist-info → rom24_quickmud_python-2.14.268.dist-info}/top_level.txt +0 -0
mud/admin_logging/admin.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from datetime import
|
|
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(
|
|
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(
|
|
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
|
@@ -40,10 +40,11 @@ from mud.models.constants import (
|
|
|
40
40
|
)
|
|
41
41
|
from mud.models.weapon_table import weapon_skill_name_for_type
|
|
42
42
|
from mud.skills import check_improve
|
|
43
|
+
from mud.skills.skill_lookup import get_skill
|
|
43
44
|
from mud.utils import rng_mm
|
|
44
45
|
from mud.utils.act import capitalize_act_line
|
|
45
46
|
from mud.wiznet import WiznetFlag, wiznet
|
|
46
|
-
from mud.world.vision import can_see_object, pers
|
|
47
|
+
from mud.world.vision import can_see_character, can_see_object, pers
|
|
47
48
|
|
|
48
49
|
HAND_TO_HAND_SKILL = "hand to hand"
|
|
49
50
|
|
|
@@ -519,6 +520,35 @@ def _mob_offensive_skill(attacker: Character, roll: int, off_flags: int, act_fla
|
|
|
519
520
|
# commented out).
|
|
520
521
|
|
|
521
522
|
|
|
523
|
+
def _compute_victim_ac(attacker: Character, victim: Character, ac_idx: int, victim_pos: int) -> int:
|
|
524
|
+
"""ROM ``one_hit`` effective victim AC (src/fight.c:480-503).
|
|
525
|
+
|
|
526
|
+
ROM divides GET_AC by 10 **first**, then applies the ``< -15`` rescale and
|
|
527
|
+
the visibility/position modifiers **on the /10 scale** — not on the raw
|
|
528
|
+
(×10) armor value. FIGHT-081: the pre-fix Python applied the modifiers and
|
|
529
|
+
the rescale to the raw AC and divided by 10 last, making the -4/+4/+6
|
|
530
|
+
modifiers ~10× too weak and mis-firing the rescale for nearly every armored
|
|
531
|
+
character. The visibility -4 gates on ``!can_see(ch, victim)`` (broader than
|
|
532
|
+
the victim's INVISIBLE affect: also blind/dark/hide, and withheld from a
|
|
533
|
+
detect-invis attacker), mirroring ROM src/fight.c:496.
|
|
534
|
+
"""
|
|
535
|
+
|
|
536
|
+
# mirroring ROM src/fight.c:480-491 — GET_AC(victim, ac) / 10 (c_div: AC is signed).
|
|
537
|
+
victim_ac = c_div(get_ac(victim, ac_idx), 10)
|
|
538
|
+
# mirroring ROM src/fight.c:493-494 — rescale of very-negative AC, on the /10 scale.
|
|
539
|
+
if victim_ac < -15:
|
|
540
|
+
victim_ac = c_div(victim_ac + 15, 5) - 15
|
|
541
|
+
# mirroring ROM src/fight.c:496-497 — !can_see(ch, victim), not the INVISIBLE affect.
|
|
542
|
+
if not can_see_character(attacker, victim):
|
|
543
|
+
victim_ac -= 4
|
|
544
|
+
# mirroring ROM src/fight.c:499-503 — position penalties on the /10 scale.
|
|
545
|
+
if victim_pos < Position.FIGHTING:
|
|
546
|
+
victim_ac += 4
|
|
547
|
+
if victim_pos < Position.RESTING:
|
|
548
|
+
victim_ac += 6
|
|
549
|
+
return victim_ac
|
|
550
|
+
|
|
551
|
+
|
|
522
552
|
def attack_round(attacker: Character, victim: Character, dt: str | int | None = None) -> str:
|
|
523
553
|
"""Resolve a single attack round.
|
|
524
554
|
|
|
@@ -548,19 +578,10 @@ def attack_round(attacker: Character, victim: Character, dt: str | int | None =
|
|
|
548
578
|
dam_type_lookup = attack_damage_type(attack_index)
|
|
549
579
|
dam_type = int(dam_type_lookup) if dam_type_lookup is not None else int(DamageType.BASH)
|
|
550
580
|
ac_idx = ac_index_for_dam_type(dam_type)
|
|
551
|
-
# ROM src/
|
|
552
|
-
victim_ac
|
|
553
|
-
#
|
|
554
|
-
|
|
555
|
-
victim_ac -= 4
|
|
556
|
-
if _victim_pos_before < Position.FIGHTING:
|
|
557
|
-
victim_ac += 4
|
|
558
|
-
if _victim_pos_before < Position.RESTING:
|
|
559
|
-
victim_ac += 6
|
|
560
|
-
|
|
561
|
-
# ROM AC clamping for very negative values
|
|
562
|
-
if victim_ac < -15:
|
|
563
|
-
victim_ac = c_div(victim_ac + 15, 5) - 15
|
|
581
|
+
# FIGHT-081: ROM one_hit (src/fight.c:480-503) divides GET_AC by 10 first,
|
|
582
|
+
# then rescales/modifies on the /10 scale. victim_ac is already the value
|
|
583
|
+
# used directly in the hit test — no further division.
|
|
584
|
+
victim_ac = _compute_victim_ac(attacker, victim, ac_idx, _victim_pos_before)
|
|
564
585
|
|
|
565
586
|
# FIGHT-019: ROM one_hit has a single attack-roll model — the THAC0 /
|
|
566
587
|
# number_bits(5) roll (src/fight.c:508-516). The legacy percent model
|
|
@@ -593,9 +614,16 @@ def attack_round(attacker: Character, victim: Character, dt: str | int | None =
|
|
|
593
614
|
)
|
|
594
615
|
else:
|
|
595
616
|
th = compute_thac0(attacker.level, attacker.ch_class, hitroll=get_hitroll(attacker), skill=skill_total)
|
|
596
|
-
|
|
617
|
+
# FIGHT-086 / HANDLER-008: ROM one_hit (src/fight.c:474-475) — backstab
|
|
618
|
+
# near-auto-hit below skill 100: thac0 -= 10 * (100 - get_skill(ch, gsn_backstab)).
|
|
619
|
+
# get_skill clamps to 0..100, so 100-skill is non-negative here. No c_div — no
|
|
620
|
+
# division. Migrated from the _backstab_skill partial mirror onto unified get_skill.
|
|
621
|
+
if _normalize_dt(dt) == "backstab":
|
|
622
|
+
th -= 10 * (100 - get_skill(attacker, "backstab"))
|
|
623
|
+
# FIGHT-081: victim_ac is already on the /10 scale (see _compute_victim_ac); no
|
|
624
|
+
# further division — ROM src/fight.c:510 uses victim_ac directly.
|
|
597
625
|
# ROM src/fight.c:510 — miss on nat 0, or (not 19 and diceroll < thac0 - victim_ac).
|
|
598
|
-
if diceroll == 0 or (diceroll != 19 and diceroll < (th -
|
|
626
|
+
if diceroll == 0 or (diceroll != 19 and diceroll < (th - victim_ac)):
|
|
599
627
|
# Miss — ROM C calls damage(ch, victim, 0, dt, dam_type, TRUE).
|
|
600
628
|
return apply_damage(attacker, victim, 0, int(dam_type), dt=attack_dt)
|
|
601
629
|
|
|
@@ -712,6 +740,19 @@ def apply_damage(
|
|
|
712
740
|
if check_shield_block(attacker, victim):
|
|
713
741
|
return capitalize_act_line(f"{pers(victim, attacker)} blocks your attack with a shield.")
|
|
714
742
|
|
|
743
|
+
# FIGHT-092 — "Inviso attacks ... not." ROM src/fight.c:763-769: a connecting
|
|
744
|
+
# hit reveals an invisible attacker. Strip the invis/mass_invis spell affects
|
|
745
|
+
# and the AFF_INVISIBLE bit, then broadcast "$n fades into existence." to the
|
|
746
|
+
# room. Placed past the parry/dodge/shield-block early-returns (mirroring ROM,
|
|
747
|
+
# where parry/dodge resolve in one_hit before damage() is entered) and before
|
|
748
|
+
# the RIV/immune modifiers, so it fires even on a 0-damage (immune) hit.
|
|
749
|
+
if attacker.has_affect(AffectFlag.INVISIBLE):
|
|
750
|
+
if hasattr(attacker, "remove_spell_effect"):
|
|
751
|
+
attacker.remove_spell_effect("invis")
|
|
752
|
+
attacker.remove_spell_effect("mass invis")
|
|
753
|
+
attacker.affected_by = int(getattr(attacker, "affected_by", 0) or 0) & ~int(AffectFlag.INVISIBLE)
|
|
754
|
+
_broadcast_pos_change(attacker, "{name} fades into existence.")
|
|
755
|
+
|
|
715
756
|
# Apply damage type resistance/vulnerability modifiers (ROM fight.c:804-816)
|
|
716
757
|
# This must happen AFTER defense checks but BEFORE damage application
|
|
717
758
|
if dam_type is not None:
|
|
@@ -1547,7 +1588,10 @@ def check_shield_block(attacker: Character, victim: Character) -> bool:
|
|
|
1547
1588
|
if not _has_shield_equipped(victim):
|
|
1548
1589
|
return False
|
|
1549
1590
|
|
|
1550
|
-
|
|
1591
|
+
# HANDLER-008: unified get_skill — NPCs get ROM's 10+2*level shield-block
|
|
1592
|
+
# formula (src/handler.c:373-432); the old dict lookup returned 0 for mobs, so
|
|
1593
|
+
# NPCs could never shield-block. PCs gate to 0 below the class skill level.
|
|
1594
|
+
shield_skill = get_skill(victim, "shield block")
|
|
1551
1595
|
chance = c_div(shield_skill, 5) + 3
|
|
1552
1596
|
|
|
1553
1597
|
# Level difference modifier
|
|
@@ -1580,7 +1624,9 @@ def check_parry(attacker: Character, victim: Character) -> bool:
|
|
|
1580
1624
|
if not is_awake(victim):
|
|
1581
1625
|
return False
|
|
1582
1626
|
|
|
1583
|
-
|
|
1627
|
+
# HANDLER-008: unified get_skill — NPCs with OFF_PARRY get ROM's level*2
|
|
1628
|
+
# (src/handler.c:373-432); PCs gate to 0 below the parry class level.
|
|
1629
|
+
parry_skill = get_skill(victim, "parry")
|
|
1584
1630
|
chance = c_div(parry_skill, 2)
|
|
1585
1631
|
|
|
1586
1632
|
has_weapon = getattr(victim, "has_weapon_equipped", False)
|
|
@@ -1590,8 +1636,11 @@ def check_parry(attacker: Character, victim: Character) -> bool:
|
|
|
1590
1636
|
else:
|
|
1591
1637
|
return False # PCs need weapons to parry
|
|
1592
1638
|
|
|
1593
|
-
# Visibility modifier
|
|
1594
|
-
|
|
1639
|
+
# Visibility modifier — FIGHT-084: ROM src/fight.c:1311 is `!can_see(ch, victim)`
|
|
1640
|
+
# (attacker→victim), NOT victim→attacker. The pre-fix `victim.can_see(attacker)`
|
|
1641
|
+
# was both the wrong direction and inert (no can_see method → `lambda x: True`
|
|
1642
|
+
# fallback never halved). check_dodge is the opposite direction (ROM :1363).
|
|
1643
|
+
if not can_see_character(attacker, victim):
|
|
1595
1644
|
chance = c_div(chance, 2)
|
|
1596
1645
|
|
|
1597
1646
|
# Level difference modifier
|
|
@@ -1623,11 +1672,17 @@ def check_dodge(attacker: Character, victim: Character) -> bool:
|
|
|
1623
1672
|
if not is_awake(victim):
|
|
1624
1673
|
return False
|
|
1625
1674
|
|
|
1626
|
-
|
|
1675
|
+
# HANDLER-008: unified get_skill — NPCs with OFF_DODGE get ROM's level*2
|
|
1676
|
+
# (src/handler.c:373-432); PCs gate to 0 below the dodge class level.
|
|
1677
|
+
dodge_skill = get_skill(victim, "dodge")
|
|
1627
1678
|
chance = c_div(dodge_skill, 2)
|
|
1628
1679
|
|
|
1629
|
-
# Visibility modifier
|
|
1630
|
-
|
|
1680
|
+
# Visibility modifier — ROM src/fight.c:1363: if (!can_see(victim, ch)) chance /= 2.
|
|
1681
|
+
# Direction is victim→attacker (defender's view of the attacker); can_see is not
|
|
1682
|
+
# symmetric. FIGHT-089 (twin of the FIGHT-084 check_parry fix): the old
|
|
1683
|
+
# getattr(victim, "can_see", lambda x: True) fallback was inert — runtime entities
|
|
1684
|
+
# have no can_see method, so the halving never fired.
|
|
1685
|
+
if not can_see_character(victim, attacker):
|
|
1631
1686
|
chance = c_div(chance, 2)
|
|
1632
1687
|
|
|
1633
1688
|
# Level difference modifier
|
mud/commands/advancement.py
CHANGED
|
@@ -165,7 +165,12 @@ def do_practice(char: Character, args: str) -> str:
|
|
|
165
165
|
if required_level is not None:
|
|
166
166
|
if required_level >= LEVEL_IMMORTAL:
|
|
167
167
|
return "You can't practice that."
|
|
168
|
-
|
|
168
|
+
# PRACTICE-002: ROM src/act_info.c:2744-2757 gates on
|
|
169
|
+
# `ch->level < skill_table[sn].skill_level[class]` UNCONDITIONALLY (part of
|
|
170
|
+
# the "You can't practice that." OR) — a below-level character cannot
|
|
171
|
+
# practice a skill even when it is already known at >=1%. The old
|
|
172
|
+
# `current <= 0 and` qualifier let a known-but-too-high skill be practiced.
|
|
173
|
+
if char.level < required_level:
|
|
169
174
|
return "You can't practice that."
|
|
170
175
|
|
|
171
176
|
rating = _rating_for_class(skill, char.ch_class)
|
mud/commands/affects.py
CHANGED
|
@@ -147,8 +147,11 @@ def do_affects(char: Character, args: str) -> str:
|
|
|
147
147
|
# Deduplication: check if same spell as previous affect
|
|
148
148
|
if paf_last and paf.type == paf_last.type:
|
|
149
149
|
if char.level >= 20:
|
|
150
|
-
#
|
|
151
|
-
|
|
150
|
+
# AFFECTS-001: ROM src/act_info.c:1726 emits exactly 22 spaces (no
|
|
151
|
+
# colon) for a duplicate affect; the ": modifies …" detail block
|
|
152
|
+
# below supplies the single colon (matching "Spell: %-15s" width).
|
|
153
|
+
# The old `+ ": "` produced a double colon (": :").
|
|
154
|
+
buf = " " * 22
|
|
152
155
|
else:
|
|
153
156
|
# Level <20: Skip duplicate spells entirely
|
|
154
157
|
continue
|
mud/commands/combat.py
CHANGED
|
@@ -25,6 +25,7 @@ from mud.models.constants import (
|
|
|
25
25
|
)
|
|
26
26
|
from mud.skills import skill_registry
|
|
27
27
|
from mud.skills.say_spell import broadcast_spell_words
|
|
28
|
+
from mud.skills.skill_lookup import get_skill
|
|
28
29
|
from mud.utils import rng_mm
|
|
29
30
|
from mud.utils.act import act_format, act_to_room
|
|
30
31
|
from mud.world.vision import can_see_character
|
|
@@ -216,11 +217,10 @@ def do_kick(char: Character, args: str) -> str:
|
|
|
216
217
|
cooldowns["kick"] = skill.cooldown
|
|
217
218
|
char.cooldowns = cooldowns
|
|
218
219
|
roll = rng_mm.number_percent()
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
chance = 0
|
|
220
|
+
# FIGHT-091 / HANDLER-008: ROM do_kick rolls get_skill(ch, gsn_kick)
|
|
221
|
+
# (src/fight.c:3125) — NPC 10+3*level, PC class-gated learned, both
|
|
222
|
+
# daze/drunk-adjusted. Migrated from the inline NPC-formula workaround.
|
|
223
|
+
chance = get_skill(char, "kick")
|
|
224
224
|
success = chance > roll
|
|
225
225
|
|
|
226
226
|
message = skill_handlers.kick(char, opponent, success=success, roll=roll)
|
|
@@ -284,7 +284,10 @@ def do_rescue(char: Character, args: str) -> str:
|
|
|
284
284
|
char.cooldowns = cooldowns
|
|
285
285
|
|
|
286
286
|
roll = rng_mm.number_percent()
|
|
287
|
-
|
|
287
|
+
# HANDLER-008: ROM do_rescue rolls get_skill(ch, gsn_rescue) (src/fight.c:3082) —
|
|
288
|
+
# PC class-gated learned, NPC 40+level (was 0 via the dict → NPC rescue never
|
|
289
|
+
# worked), daze/drunk-adjusted.
|
|
290
|
+
learned = get_skill(char, "rescue")
|
|
288
291
|
success = roll <= learned
|
|
289
292
|
|
|
290
293
|
if not success:
|
|
@@ -1173,29 +1176,38 @@ def do_dirt(char: Character, args: str) -> str:
|
|
|
1173
1176
|
return result
|
|
1174
1177
|
|
|
1175
1178
|
|
|
1176
|
-
def do_trip(char: Character, args: str) -> str:
|
|
1179
|
+
def do_trip(char: Character, args: str, *, victim: Character | None = None) -> str:
|
|
1177
1180
|
"""
|
|
1178
1181
|
Trip opponent to knock them down.
|
|
1179
1182
|
|
|
1180
1183
|
ROM Reference: src/fight.c do_trip (lines 2641-2760)
|
|
1184
|
+
|
|
1185
|
+
FIGHT-090: this is the single canonical trip implementation. The ``victim``
|
|
1186
|
+
keyword lets the skill-registry ``trip`` handler (``skill_handlers.trip``)
|
|
1187
|
+
delegate here with an explicit target instead of maintaining a divergent copy.
|
|
1181
1188
|
"""
|
|
1182
1189
|
target_name = (args or "").strip()
|
|
1183
1190
|
|
|
1184
|
-
# Check if character has the skill. ROM src/fight.c do_trip: NPCs gate on
|
|
1185
|
-
# OFF_TRIP (no skills dict), PCs on the learned percent.
|
|
1186
|
-
# get_skill
|
|
1187
|
-
#
|
|
1191
|
+
# Check if character has the skill. ROM src/fight.c:2649 do_trip: NPCs gate on
|
|
1192
|
+
# OFF_TRIP (no skills dict), PCs on the learned percent. HANDLER-008: ROM's
|
|
1193
|
+
# `chance = get_skill(ch, gsn_trip)` gives an OFF_TRIP NPC `10 + 3*level`
|
|
1194
|
+
# (src/handler.c:373-432), NOT 100 — the old hardcode let a low-level mob trip
|
|
1195
|
+
# far too often. The OFF_TRIP gate stays (get_skill's NPC dispatch returns 0
|
|
1196
|
+
# without the flag, but the explicit gate mirrors ROM's IS_SET check + the ""
|
|
1197
|
+
# silent return for descriptor-less mobs).
|
|
1188
1198
|
if getattr(char, "is_npc", False):
|
|
1189
1199
|
if not _npc_off_flags(char) & OffFlag.TRIP:
|
|
1190
1200
|
return ""
|
|
1191
|
-
skill_level =
|
|
1201
|
+
skill_level = get_skill(char, "trip")
|
|
1192
1202
|
else:
|
|
1193
1203
|
skill_level = _character_skill_percent(char, "trip")
|
|
1194
1204
|
if skill_level == 0:
|
|
1195
1205
|
return "Tripping? What's that?"
|
|
1196
1206
|
|
|
1197
|
-
# Find target
|
|
1198
|
-
if not
|
|
1207
|
+
# Find target (skip when an explicit victim is supplied via delegation).
|
|
1208
|
+
if victim is not None:
|
|
1209
|
+
pass
|
|
1210
|
+
elif not target_name:
|
|
1199
1211
|
victim = getattr(char, "fighting", None)
|
|
1200
1212
|
if victim is None:
|
|
1201
1213
|
return "But you aren't fighting anyone!"
|
|
@@ -1221,19 +1233,38 @@ def do_trip(char: Character, args: str) -> str:
|
|
|
1221
1233
|
):
|
|
1222
1234
|
return "Kill stealing is not permitted."
|
|
1223
1235
|
|
|
1224
|
-
# Can't trip flying targets
|
|
1236
|
+
# Can't trip flying targets — ROM src/fight.c:2686 act("$S feet aren't on the
|
|
1237
|
+
# ground.", ch, NULL, victim, TO_CHAR); $S = victim's possessive pronoun.
|
|
1238
|
+
# FIGHT-090: was a baked "Their" here (the PERS render was only on
|
|
1239
|
+
# skill_handlers.trip via TRIP-001); merged in so do_trip is canonical.
|
|
1225
1240
|
victim_affected = getattr(victim, "affected_by", 0)
|
|
1226
1241
|
if victim_affected & AffectFlag.FLYING:
|
|
1227
|
-
return "
|
|
1242
|
+
return act_format("$S feet aren't on the ground.", recipient=char, actor=char, arg2=victim)
|
|
1228
1243
|
|
|
1229
|
-
# Can't trip someone already down
|
|
1244
|
+
# Can't trip someone already down — ROM src/fight.c:2689 act("$N is already
|
|
1245
|
+
# down.", …, TO_CHAR); $N = PERS(victim). FIGHT-090 (was baked "They are").
|
|
1230
1246
|
victim_pos = getattr(victim, "position", Position.STANDING)
|
|
1231
1247
|
if victim_pos < Position.FIGHTING:
|
|
1232
|
-
return "
|
|
1248
|
+
return act_format("$N is already down.", recipient=char, actor=char, arg2=victim)
|
|
1249
|
+
|
|
1250
|
+
# FIGHT-082: ROM src/fight.c:2700/2742/2750 — do_trip WAIT_STATE uses the
|
|
1251
|
+
# skill's RAW beats (skill_table[gsn_trip].beats == 24), not PULSE_VIOLENCE,
|
|
1252
|
+
# with no HASTE/SLOW adjustment (read skill.beats directly, per CAST-010; the
|
|
1253
|
+
# _compute_skill_lag haste/slow scaling is FIGHT-085's separate concern and
|
|
1254
|
+
# must stay out of ROM WAIT_STATE sites).
|
|
1255
|
+
_trip_skill = skill_registry.get("trip")
|
|
1256
|
+
trip_beats = int(getattr(_trip_skill, "beats", 0) or 0)
|
|
1233
1257
|
|
|
1234
1258
|
if victim is char:
|
|
1235
|
-
|
|
1236
|
-
|
|
1259
|
+
# ROM src/fight.c:2697-2703 (do_trip self-trip): colored TO_CHAR + a room
|
|
1260
|
+
# broadcast ($n PERS-masked, $s possessive). FIGHT-090: the room line +
|
|
1261
|
+
# {5..{x colour were on skill_handlers.trip (FIGHT-039) but not here — merged
|
|
1262
|
+
# in so this path is the complete canonical implementation.
|
|
1263
|
+
skill_registry._apply_wait_state(char, 2 * trip_beats)
|
|
1264
|
+
self_room = getattr(char, "room", None)
|
|
1265
|
+
if self_room is not None:
|
|
1266
|
+
act_to_room(self_room, "{5$n trips over $s own feet!{x", char, exclude=char)
|
|
1267
|
+
return "{5You fall flat on your face!{x"
|
|
1237
1268
|
|
|
1238
1269
|
# FIGHT-071: ROM src/fight.c:2705-2709 — do_trip's charm gate, checked LAST
|
|
1239
1270
|
# (after flying/position/self), with act("$N is your beloved master.", …).
|
|
@@ -1258,6 +1289,13 @@ def do_trip(char: Character, args: str) -> str:
|
|
|
1258
1289
|
victim_dex = victim.get_curr_stat(Stat.DEX) or 0
|
|
1259
1290
|
chance += char_dex - victim_dex * 3 // 2
|
|
1260
1291
|
|
|
1292
|
+
# Speed modifier — ROM src/fight.c:2722-2726: attacker OFF_FAST/AFF_HASTE
|
|
1293
|
+
# adds +10, victim OFF_FAST/AFF_HASTE subtracts 20.
|
|
1294
|
+
if (_npc_off_flags(char) & OffFlag.FAST) or char.has_affect(AffectFlag.HASTE):
|
|
1295
|
+
chance += 10
|
|
1296
|
+
if (_npc_off_flags(victim) & OffFlag.FAST) or victim.has_affect(AffectFlag.HASTE):
|
|
1297
|
+
chance -= 20
|
|
1298
|
+
|
|
1261
1299
|
# Level modifier
|
|
1262
1300
|
char_level = skill_handlers._coerce_int(getattr(char, "level", 1))
|
|
1263
1301
|
victim_level = skill_handlers._coerce_int(getattr(victim, "level", 1))
|
|
@@ -1265,19 +1303,44 @@ def do_trip(char: Character, args: str) -> str:
|
|
|
1265
1303
|
|
|
1266
1304
|
# Roll
|
|
1267
1305
|
if rng_mm.number_percent() < chance:
|
|
1268
|
-
# Success
|
|
1306
|
+
# Success — ROM src/fight.c:2735-2745.
|
|
1307
|
+
# FIGHT-088: ROM emits three act() lines before the damage() call. TO_VICT is
|
|
1308
|
+
# pushed to the victim, TO_NOTVICT broadcasts to the room ($M = victim's
|
|
1309
|
+
# objective pronoun), TO_CHAR is this command's return value. Replaces a
|
|
1310
|
+
# single baked f-string with no $N/$M render and no room broadcast.
|
|
1311
|
+
from mud.utils.messaging import send_to_char_buffered as _send
|
|
1312
|
+
|
|
1313
|
+
_send(victim, act_format("{5$n trips you and you go down!{x", recipient=victim, actor=char, arg2=victim))
|
|
1314
|
+
room = getattr(char, "room", None)
|
|
1315
|
+
if room is not None:
|
|
1316
|
+
act_to_room(room, "{5$n trips $N, sending $M to the ground.{x", char, arg2=victim, exclude=victim)
|
|
1317
|
+
|
|
1318
|
+
# DAZE_STATE(victim, 2 * PULSE_VIOLENCE) — the victim is DAZED, not WAIT'd.
|
|
1319
|
+
victim.daze = max(int(getattr(victim, "daze", 0) or 0), 2 * get_pulse_violence())
|
|
1320
|
+
# WAIT_STATE(ch, skill_table[gsn_trip].beats) — raw beats, not PULSE_VIOLENCE.
|
|
1321
|
+
skill_registry._apply_wait_state(char, trip_beats)
|
|
1269
1322
|
victim.position = Position.RESTING
|
|
1270
|
-
skill_registry._apply_wait_state(char, get_pulse_violence())
|
|
1271
|
-
skill_registry._apply_wait_state(victim, get_pulse_violence() * 2)
|
|
1272
1323
|
|
|
1273
|
-
# Damage
|
|
1274
|
-
|
|
1275
|
-
|
|
1324
|
+
# Damage — ROM :2744 number_range(2, 2 + 2 * victim->size); no skill-level term.
|
|
1325
|
+
# dt=gsn_trip so the dam_message renders the trip verb (ROM passes gsn_trip).
|
|
1326
|
+
damage_amt = rng_mm.number_range(2, 2 + 2 * victim_size)
|
|
1327
|
+
apply_damage(char, victim, damage_amt, DamageType.BASH, dt="trip")
|
|
1276
1328
|
|
|
1277
|
-
|
|
1329
|
+
# ROM src/fight.c:2739 — check_improve(ch, gsn_trip, TRUE, 1). FIGHT-090:
|
|
1330
|
+
# do_trip was missing check_improve entirely (it was only on the duplicate
|
|
1331
|
+
# skill_handlers.trip); merged in so trip improvement works via the command.
|
|
1332
|
+
skill_registry._check_improve(char, _trip_skill, "trip", True)
|
|
1333
|
+
message = act_format("{5You trip $N and $N goes down!{x", recipient=char, actor=char, arg2=victim)
|
|
1278
1334
|
else:
|
|
1279
|
-
|
|
1280
|
-
|
|
1335
|
+
# FIGHT-088: ROM :2749 — the failure branch calls damage(ch, victim, 0,
|
|
1336
|
+
# gsn_trip, DAM_BASH, TRUE) BEFORE the wait, delivering the miss dam_message
|
|
1337
|
+
# and starting the fight (set_fighting). apply_damage returns the attacker's
|
|
1338
|
+
# TO_CHAR line unpushed (single-delivery), so returning it is correct.
|
|
1339
|
+
message = apply_damage(char, victim, 0, DamageType.BASH, dt="trip")
|
|
1340
|
+
# ROM :2750 — WAIT_STATE(ch, skill_table[gsn_trip].beats * 2 / 3), after damage.
|
|
1341
|
+
skill_registry._apply_wait_state(char, trip_beats * 2 // 3)
|
|
1342
|
+
# ROM src/fight.c:2752 — check_improve(ch, gsn_trip, FALSE, 1). FIGHT-090.
|
|
1343
|
+
skill_registry._check_improve(char, _trip_skill, "trip", False)
|
|
1281
1344
|
|
|
1282
1345
|
check_killer(char, victim) # mirroring ROM src/fight.c:2753 — unconditional
|
|
1283
1346
|
return message
|
mud/commands/consumption.py
CHANGED
|
@@ -12,6 +12,7 @@ from mud.math.c_compat import c_div
|
|
|
12
12
|
from mud.models.constants import AffectFlag, ItemType
|
|
13
13
|
from mud.utils.act import act_to_room
|
|
14
14
|
from mud.utils.rng_mm import number_fuzzy
|
|
15
|
+
from mud.world.obj_find import get_obj_carry
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
from mud.models.character import Character
|
|
@@ -36,8 +37,9 @@ def do_eat(ch: Character, args: str) -> str:
|
|
|
36
37
|
if not args:
|
|
37
38
|
return "Eat what?"
|
|
38
39
|
|
|
39
|
-
# Find object in inventory
|
|
40
|
-
|
|
40
|
+
# Find object in inventory — ROM src/act_obj.c:1296 uses get_obj_carry, which
|
|
41
|
+
# parses the `N.name` count prefix and matches keywords (EAT-008).
|
|
42
|
+
obj = get_obj_carry(ch, args)
|
|
41
43
|
if not obj:
|
|
42
44
|
return "You do not have that item."
|
|
43
45
|
|
|
@@ -321,32 +323,6 @@ def do_drink(ch: Character, args: str) -> str:
|
|
|
321
323
|
return "\n".join(messages)
|
|
322
324
|
|
|
323
325
|
|
|
324
|
-
def _find_obj_inventory(ch: Character, name: str) -> Object | None:
|
|
325
|
-
"""Find an object in character's inventory by name.
|
|
326
|
-
|
|
327
|
-
ROM Reference: src/handler.c get_obj_carry — searches `ch->carrying`.
|
|
328
|
-
QuickMUD stores the carrying list on `Character.inventory` (see
|
|
329
|
-
`mud/models/character.py:396`).
|
|
330
|
-
"""
|
|
331
|
-
inventory = ch.inventory
|
|
332
|
-
if not inventory or not name:
|
|
333
|
-
return None
|
|
334
|
-
|
|
335
|
-
name_lower = name.lower()
|
|
336
|
-
for obj in inventory:
|
|
337
|
-
# Check short description
|
|
338
|
-
short_descr = getattr(obj, "short_descr", "")
|
|
339
|
-
if name_lower in short_descr.lower():
|
|
340
|
-
return obj
|
|
341
|
-
|
|
342
|
-
# Check name
|
|
343
|
-
obj_name = getattr(obj, "name", "")
|
|
344
|
-
if name_lower in obj_name.lower():
|
|
345
|
-
return obj
|
|
346
|
-
|
|
347
|
-
return None
|
|
348
|
-
|
|
349
|
-
|
|
350
326
|
def _destroy_object(ch: Character, obj: Object) -> None:
|
|
351
327
|
"""Remove an object from character's inventory and destroy it.
|
|
352
328
|
|
mud/commands/doors.py
CHANGED
|
@@ -163,10 +163,13 @@ def do_open(char: Character, args: str) -> str:
|
|
|
163
163
|
|
|
164
164
|
# Container
|
|
165
165
|
if item_type == ItemType.CONTAINER:
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
# MOVE-008: ROM do_open (src/act_move.c) checks CONT_CLOSED before
|
|
167
|
+
# CONT_CLOSEABLE, so an already-open non-closeable container reports
|
|
168
|
+
# "It's already open.", not "You can't do that."
|
|
168
169
|
if not (values[1] & ContainerFlag.CLOSED):
|
|
169
170
|
return "It's already open."
|
|
171
|
+
if not (values[1] & ContainerFlag.CLOSEABLE):
|
|
172
|
+
return "You can't do that."
|
|
170
173
|
if values[1] & ContainerFlag.LOCKED:
|
|
171
174
|
return "It's locked."
|
|
172
175
|
|
|
@@ -258,10 +261,13 @@ def do_close(char: Character, args: str) -> str:
|
|
|
258
261
|
|
|
259
262
|
# Container
|
|
260
263
|
if item_type == ItemType.CONTAINER:
|
|
261
|
-
|
|
262
|
-
|
|
264
|
+
# MOVE-008: ROM do_close (src/act_move.c) checks CONT_CLOSED before
|
|
265
|
+
# CONT_CLOSEABLE, so an already-closed non-closeable container reports
|
|
266
|
+
# "It's already closed.", not "You can't do that."
|
|
263
267
|
if values[1] & ContainerFlag.CLOSED:
|
|
264
268
|
return "It's already closed."
|
|
269
|
+
if not (values[1] & ContainerFlag.CLOSEABLE):
|
|
270
|
+
return "You can't do that."
|
|
265
271
|
|
|
266
272
|
obj.value[1] = values[1] | ContainerFlag.CLOSED
|
|
267
273
|
obj_name = getattr(obj, "short_descr", None) or getattr(obj, "name", "it")
|
|
@@ -655,9 +661,13 @@ def do_pick(char: Character, args: str) -> str:
|
|
|
655
661
|
exit_info = getattr(pexit, "exit_info", 0)
|
|
656
662
|
key_vnum = getattr(pexit, "key", 0)
|
|
657
663
|
|
|
658
|
-
# Immortal check (ROM C lines 958, 963, 973)
|
|
659
|
-
|
|
660
|
-
|
|
664
|
+
# Immortal check (ROM C lines 958, 963, 973): !IS_IMMORTAL(ch), where
|
|
665
|
+
# IS_IMMORTAL = get_trust(ch) >= LEVEL_IMMORTAL (=52, src/merc.h:149,2091)
|
|
666
|
+
# and get_trust falls back to ch->level when ch->trust is unset. The prior
|
|
667
|
+
# `trust >= 51` was wrong twice: 51 is LEVEL_HERO (a mortal), and reading raw
|
|
668
|
+
# `char.trust` skipped the level fallback (a level-52 immortal with trust 0
|
|
669
|
+
# was wrongly refused). Character.is_immortal() encodes both correctly.
|
|
670
|
+
is_immortal = char.is_immortal()
|
|
661
671
|
|
|
662
672
|
if not (exit_info & EX_CLOSED) and not is_immortal:
|
|
663
673
|
return "It's not closed."
|
mud/commands/equipment.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
|
|
|
11
11
|
from mud.handler import equip_char, unequip_char
|
|
12
12
|
from mud.models.constants import ExtraFlag, ItemType, Position, WearFlag, WearLocation
|
|
13
13
|
from mud.utils.act import act_to_room
|
|
14
|
+
from mud.world.obj_find import get_obj_carry
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
from mud.models.character import Character
|
|
@@ -155,8 +156,10 @@ def do_wear(ch: Character, args: str) -> str:
|
|
|
155
156
|
if args.lower() == "all":
|
|
156
157
|
return _wear_all(ch)
|
|
157
158
|
|
|
158
|
-
# Find object in inventory
|
|
159
|
-
|
|
159
|
+
# Find object in inventory — ROM src/act_obj.c:1726 uses get_obj_carry, which
|
|
160
|
+
# parses the `N.name` count prefix and matches keywords (WEAR-010). wield/hold
|
|
161
|
+
# delegate to do_wear, so this covers all three.
|
|
162
|
+
obj = get_obj_carry(ch, args)
|
|
160
163
|
if not obj:
|
|
161
164
|
return "You do not have that item."
|
|
162
165
|
|
|
@@ -660,24 +663,3 @@ def _get_wear_location(obj: Object, wear_flags: int, ch: Character | None = None
|
|
|
660
663
|
return WearLocation.FLOAT
|
|
661
664
|
|
|
662
665
|
return None
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
def _find_obj_inventory(ch: Character, name: str) -> Object | None:
|
|
666
|
-
"""Find an object in character's inventory by name."""
|
|
667
|
-
inventory = getattr(ch, "inventory", [])
|
|
668
|
-
if not inventory or not name:
|
|
669
|
-
return None
|
|
670
|
-
|
|
671
|
-
name_lower = name.lower()
|
|
672
|
-
for obj in inventory:
|
|
673
|
-
# Check short description
|
|
674
|
-
short_descr = getattr(obj, "short_descr", "")
|
|
675
|
-
if name_lower in short_descr.lower():
|
|
676
|
-
return obj
|
|
677
|
-
|
|
678
|
-
# Check name
|
|
679
|
-
obj_name = getattr(obj, "name", "")
|
|
680
|
-
if name_lower in obj_name.lower():
|
|
681
|
-
return obj
|
|
682
|
-
|
|
683
|
-
return None
|