rom24-quickmud-python 2.4.2__py3-none-any.whl → 2.5.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/affects/saves.py +5 -5
- mud/ai/aggressive.py +5 -0
- mud/combat/__init__.py +2 -1
- mud/combat/assist.py +205 -0
- mud/combat/engine.py +24 -0
- mud/commands/combat.py +88 -116
- mud/commands/equipment.py +50 -1
- mud/commands/healer.py +10 -10
- mud/commands/inventory.py +8 -0
- mud/commands/magic_items.py +1 -2
- mud/commands/obj_manipulation.py +8 -1
- mud/game_loop.py +19 -13
- mud/models/character.py +27 -26
- mud/models/constants.py +1 -1
- mud/skills/handlers.py +24 -22
- mud/skills/say_spell.py +153 -0
- mud/spawning/reset_handler.py +16 -6
- mud/world/world_state.py +3 -9
- {rom24_quickmud_python-2.4.2.dist-info → rom24_quickmud_python-2.5.1.dist-info}/METADATA +20 -11
- {rom24_quickmud_python-2.4.2.dist-info → rom24_quickmud_python-2.5.1.dist-info}/RECORD +24 -22
- {rom24_quickmud_python-2.4.2.dist-info → rom24_quickmud_python-2.5.1.dist-info}/WHEEL +0 -0
- {rom24_quickmud_python-2.4.2.dist-info → rom24_quickmud_python-2.5.1.dist-info}/entry_points.txt +0 -0
- {rom24_quickmud_python-2.4.2.dist-info → rom24_quickmud_python-2.5.1.dist-info}/licenses/LICENSE +0 -0
- {rom24_quickmud_python-2.4.2.dist-info → rom24_quickmud_python-2.5.1.dist-info}/top_level.txt +0 -0
mud/affects/saves.py
CHANGED
|
@@ -90,7 +90,10 @@ def _check_immune(victim: Character, dam_type: int) -> int:
|
|
|
90
90
|
immune = IS_IMMUNE
|
|
91
91
|
elif (victim.res_flags & bit) and immune != IS_IMMUNE:
|
|
92
92
|
immune = IS_RESISTANT
|
|
93
|
-
|
|
93
|
+
|
|
94
|
+
# ROM C handler.c:306-314 - vuln check runs AFTER imm/res checks
|
|
95
|
+
# This is NOT elif - it runs independently to allow downgrading immunity
|
|
96
|
+
if victim.vuln_flags & bit:
|
|
94
97
|
if immune == IS_IMMUNE:
|
|
95
98
|
immune = IS_RESISTANT
|
|
96
99
|
elif immune == IS_RESISTANT:
|
|
@@ -126,10 +129,7 @@ def saves_spell(level: int, victim: Character, dam_type: int) -> bool:
|
|
|
126
129
|
save -= 2
|
|
127
130
|
|
|
128
131
|
# Not NPC → apply fMana reduction if class gains mana
|
|
129
|
-
if (
|
|
130
|
-
not victim.is_npc
|
|
131
|
-
and FMANA_BY_CLASS.get(victim.ch_class, False)
|
|
132
|
-
):
|
|
132
|
+
if not victim.is_npc and FMANA_BY_CLASS.get(victim.ch_class, False):
|
|
133
133
|
save = c_div(9 * save, 10)
|
|
134
134
|
|
|
135
135
|
save = urange(5, save, 95)
|
mud/ai/aggressive.py
CHANGED
mud/combat/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Combat engine utilities."""
|
|
2
2
|
|
|
3
|
+
from .assist import check_assist
|
|
3
4
|
from .engine import attack_round, multi_hit
|
|
4
5
|
from .messages import dam_message
|
|
5
6
|
|
|
6
|
-
__all__ = ["attack_round", "
|
|
7
|
+
__all__ = ["attack_round", "check_assist", "dam_message", "multi_hit"]
|
mud/combat/assist.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Combat assist mechanics - auto-assist for group combat.
|
|
3
|
+
|
|
4
|
+
ROM Reference: src/fight.c check_assist (lines 105-181)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from mud.characters import is_same_group
|
|
12
|
+
from mud.combat.engine import multi_hit, is_good, is_evil, is_neutral
|
|
13
|
+
from mud.combat.safety import is_safe
|
|
14
|
+
from mud.models.constants import AffectFlag, OffFlag, PlayerFlag
|
|
15
|
+
from mud.utils import rng_mm
|
|
16
|
+
from mud.world.vision import can_see_character
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from mud.models.character import Character
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_assist(ch: Character, victim: Character) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Check for auto-assist in combat following ROM C src/fight.c:check_assist (L105-181).
|
|
25
|
+
|
|
26
|
+
This function is called when 'ch' attacks 'victim' to see if anyone in the room
|
|
27
|
+
will automatically assist either side.
|
|
28
|
+
|
|
29
|
+
Handles six assist types:
|
|
30
|
+
1. ASSIST_PLAYERS: Mobs help players fighting weaker mobs (lines 116-124)
|
|
31
|
+
2. PLR_AUTOASSIST: Players auto-assist group members (lines 126-135)
|
|
32
|
+
3. ASSIST_ALL: Mobs assist any mob in the room (line 141)
|
|
33
|
+
4. ASSIST_RACE: Mobs assist same race (lines 143-144)
|
|
34
|
+
5. ASSIST_ALIGN: Mobs assist same alignment (lines 145-148)
|
|
35
|
+
6. ASSIST_VNUM: Mobs assist same vnum (lines 149-150)
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
ch: Character currently attacking (the aggressor)
|
|
39
|
+
victim: Character being attacked by ch
|
|
40
|
+
|
|
41
|
+
ROM C Reference:
|
|
42
|
+
src/fight.c:105-181
|
|
43
|
+
"""
|
|
44
|
+
# Get all characters in the same room
|
|
45
|
+
room = getattr(ch, "room", None)
|
|
46
|
+
if not room:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
people_in_room = getattr(room, "people", [])
|
|
50
|
+
if not people_in_room:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# Loop through all characters in the room
|
|
54
|
+
# NOTE: We need to be careful about list modification during iteration
|
|
55
|
+
# ROM uses rch_next pattern to handle this
|
|
56
|
+
for rch in list(people_in_room): # Create a copy to avoid modification issues
|
|
57
|
+
# Skip if not awake or already fighting
|
|
58
|
+
if not _is_awake(rch):
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if getattr(rch, "fighting", None) is not None:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
# --- ASSIST_PLAYERS: Mobs help players fighting weaker mobs (ROM lines 116-124) ---
|
|
65
|
+
if not _is_npc(ch) and _is_npc(rch):
|
|
66
|
+
rch_off_flags = getattr(rch, "off_flags", 0)
|
|
67
|
+
if rch_off_flags & OffFlag.ASSIST_PLAYERS:
|
|
68
|
+
rch_level = getattr(rch, "level", 1)
|
|
69
|
+
victim_level = getattr(victim, "level", 1)
|
|
70
|
+
if rch_level + 6 > victim_level:
|
|
71
|
+
_emote(rch, "screams and attacks!")
|
|
72
|
+
multi_hit(rch, victim, None)
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# --- PCs next (ROM lines 126-135) ---
|
|
76
|
+
# Player characters or charmed mobs can auto-assist
|
|
77
|
+
if not _is_npc(ch) or _is_affected(ch, AffectFlag.CHARM):
|
|
78
|
+
# Check if rch is a player with autoassist or a charmed mob
|
|
79
|
+
rch_act = getattr(rch, "act", 0)
|
|
80
|
+
is_rch_autoassist = not _is_npc(rch) and (rch_act & PlayerFlag.AUTOASSIST)
|
|
81
|
+
is_rch_charmed = _is_affected(rch, AffectFlag.CHARM)
|
|
82
|
+
|
|
83
|
+
if (is_rch_autoassist or is_rch_charmed) and is_same_group(ch, rch):
|
|
84
|
+
if not is_safe(rch, victim):
|
|
85
|
+
multi_hit(rch, victim, None)
|
|
86
|
+
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# --- NPC assist cases (ROM lines 137-178) ---
|
|
90
|
+
# Only NPCs that aren't charmed can use these assist types
|
|
91
|
+
if _is_npc(ch) and not _is_affected(ch, AffectFlag.CHARM):
|
|
92
|
+
if not _is_npc(rch):
|
|
93
|
+
continue # rch must be NPC for these assist types
|
|
94
|
+
|
|
95
|
+
rch_off_flags = getattr(rch, "off_flags", 0)
|
|
96
|
+
should_assist = False
|
|
97
|
+
|
|
98
|
+
# ASSIST_ALL: Assist any mob
|
|
99
|
+
if rch_off_flags & OffFlag.ASSIST_ALL:
|
|
100
|
+
should_assist = True
|
|
101
|
+
|
|
102
|
+
# ASSIST_RACE: Assist same race
|
|
103
|
+
elif rch_off_flags & OffFlag.ASSIST_RACE:
|
|
104
|
+
ch_race = getattr(ch, "race", None)
|
|
105
|
+
rch_race = getattr(rch, "race", None)
|
|
106
|
+
if ch_race and rch_race and ch_race == rch_race:
|
|
107
|
+
should_assist = True
|
|
108
|
+
|
|
109
|
+
# ASSIST_ALIGN: Assist same alignment
|
|
110
|
+
elif rch_off_flags & OffFlag.ASSIST_ALIGN:
|
|
111
|
+
if (
|
|
112
|
+
(is_good(rch) and is_good(ch))
|
|
113
|
+
or (is_evil(rch) and is_evil(ch))
|
|
114
|
+
or (is_neutral(rch) and is_neutral(ch))
|
|
115
|
+
):
|
|
116
|
+
should_assist = True
|
|
117
|
+
|
|
118
|
+
# ASSIST_VNUM: Assist same vnum (same mob prototype)
|
|
119
|
+
elif rch_off_flags & OffFlag.ASSIST_VNUM:
|
|
120
|
+
ch_vnum = getattr(ch, "vnum", None)
|
|
121
|
+
rch_vnum = getattr(rch, "vnum", None)
|
|
122
|
+
if ch_vnum is not None and rch_vnum is not None and ch_vnum == rch_vnum:
|
|
123
|
+
should_assist = True
|
|
124
|
+
|
|
125
|
+
# Group assist: NPCs in same group help each other
|
|
126
|
+
ch_group = getattr(ch, "group", None)
|
|
127
|
+
rch_group = getattr(rch, "group", None)
|
|
128
|
+
if ch_group and rch_group and ch_group == rch_group:
|
|
129
|
+
should_assist = True
|
|
130
|
+
|
|
131
|
+
if should_assist:
|
|
132
|
+
# ROM lines 156-157: 50% chance to skip assist
|
|
133
|
+
if rng_mm.number_bits(1) == 0:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# ROM lines 159-170: Randomly select target from victim's group
|
|
137
|
+
target = None
|
|
138
|
+
number = 0
|
|
139
|
+
|
|
140
|
+
# Use reservoir sampling to pick random group member
|
|
141
|
+
for vch in list(people_in_room):
|
|
142
|
+
if can_see_character(rch, vch) and is_same_group(vch, victim):
|
|
143
|
+
if rng_mm.number_range(0, number) == 0:
|
|
144
|
+
target = vch
|
|
145
|
+
number += 1
|
|
146
|
+
|
|
147
|
+
# ROM lines 172-176: Attack the selected target
|
|
148
|
+
if target is not None:
|
|
149
|
+
_emote(rch, "screams and attacks!")
|
|
150
|
+
multi_hit(rch, target, None)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _is_awake(char: Character) -> bool:
|
|
154
|
+
"""Check if character is awake (ROM IS_AWAKE macro)."""
|
|
155
|
+
from mud.models.constants import Position
|
|
156
|
+
|
|
157
|
+
position = getattr(char, "position", Position.STANDING)
|
|
158
|
+
return position > Position.SLEEPING
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _is_npc(char: Character) -> bool:
|
|
162
|
+
"""Check if character is NPC (ROM IS_NPC macro)."""
|
|
163
|
+
return getattr(char, "is_npc", False)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _is_affected(char: Character, flag: AffectFlag) -> bool:
|
|
167
|
+
"""Check if character has affect flag (ROM IS_AFFECTED macro)."""
|
|
168
|
+
affected_by = getattr(char, "affected_by", 0)
|
|
169
|
+
return bool(affected_by & flag)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _emote(char: Character, message: str) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Make character perform an emote.
|
|
175
|
+
|
|
176
|
+
ROM C uses: do_function(rch, &do_emote, "screams and attacks!");
|
|
177
|
+
Python equivalent: Send message to room.
|
|
178
|
+
"""
|
|
179
|
+
from mud.models.social import expand_placeholders
|
|
180
|
+
|
|
181
|
+
room = getattr(char, "room", None)
|
|
182
|
+
if not room:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Format: "Name screams and attacks!"
|
|
186
|
+
char_name = getattr(char, "name", "Someone")
|
|
187
|
+
full_message = f"{char_name} {message}"
|
|
188
|
+
|
|
189
|
+
# Send to everyone in the room
|
|
190
|
+
people = getattr(room, "people", [])
|
|
191
|
+
for person in people:
|
|
192
|
+
if person != char:
|
|
193
|
+
_send_to_char(person, full_message)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _send_to_char(char: Character, message: str) -> None:
|
|
197
|
+
"""Send a message to a character."""
|
|
198
|
+
send = getattr(char, "send", None)
|
|
199
|
+
if callable(send):
|
|
200
|
+
send(message + "\n")
|
|
201
|
+
else:
|
|
202
|
+
# Fallback: add to messages list if it exists
|
|
203
|
+
messages = getattr(char, "messages", None)
|
|
204
|
+
if isinstance(messages, list):
|
|
205
|
+
messages.append(message + "\n")
|
mud/combat/engine.py
CHANGED
|
@@ -307,6 +307,11 @@ def multi_hit(attacker: Character, victim: Character, dt: str | int | None = Non
|
|
|
307
307
|
if victim.position == Position.DEAD or not hasattr(attacker, "fighting") or attacker.fighting != victim:
|
|
308
308
|
return results
|
|
309
309
|
|
|
310
|
+
# ROM src/fight.c:90 - Check for assist after first attack
|
|
311
|
+
from mud.combat.assist import check_assist
|
|
312
|
+
|
|
313
|
+
check_assist(attacker, victim)
|
|
314
|
+
|
|
310
315
|
# ROM allows only a single strike for backstab.
|
|
311
316
|
if _normalize_dt(dt) == "backstab":
|
|
312
317
|
return results
|
|
@@ -475,6 +480,7 @@ def apply_damage(
|
|
|
475
480
|
|
|
476
481
|
Handles:
|
|
477
482
|
- Defense checks (parry, dodge, shield_block) - ROM checks these AFTER hit but BEFORE damage
|
|
483
|
+
- Damage type resistance/vulnerability (ROM fight.c:804-816)
|
|
478
484
|
- Damage application and hit point reduction
|
|
479
485
|
- Position updates based on remaining hit points
|
|
480
486
|
- Fighting state management (set_fighting, stop_fighting)
|
|
@@ -504,6 +510,24 @@ def apply_damage(
|
|
|
504
510
|
if check_dodge(attacker, victim):
|
|
505
511
|
return f"{victim.name} dodges your attack."
|
|
506
512
|
|
|
513
|
+
# Apply damage type resistance/vulnerability modifiers (ROM fight.c:804-816)
|
|
514
|
+
# This must happen AFTER defense checks but BEFORE damage application
|
|
515
|
+
if dam_type is not None:
|
|
516
|
+
IS_IMMUNE = 1
|
|
517
|
+
IS_RESISTANT = 2
|
|
518
|
+
IS_VULNERABLE = 3
|
|
519
|
+
|
|
520
|
+
immune_check = _riv_check(victim, dam_type)
|
|
521
|
+
if immune_check == IS_IMMUNE:
|
|
522
|
+
immune = True
|
|
523
|
+
damage = 0
|
|
524
|
+
elif immune_check == IS_RESISTANT:
|
|
525
|
+
# ROM: dam -= dam / 3 (reduces damage by 33%)
|
|
526
|
+
damage -= c_div(damage, 3)
|
|
527
|
+
elif immune_check == IS_VULNERABLE:
|
|
528
|
+
# ROM: dam += dam / 2 (increases damage by 50%)
|
|
529
|
+
damage += c_div(damage, 2)
|
|
530
|
+
|
|
507
531
|
message_bundle: DamageMessages | None = None
|
|
508
532
|
if show:
|
|
509
533
|
message_bundle = dam_message(attacker, victim, damage, dt, immune)
|
mud/commands/combat.py
CHANGED
|
@@ -246,14 +246,17 @@ def do_rescue(char: Character, args: str) -> str:
|
|
|
246
246
|
|
|
247
247
|
|
|
248
248
|
def _find_room_target(char: Character, name: str) -> Character | None:
|
|
249
|
+
"""Find a character in the same room by name.
|
|
250
|
+
|
|
251
|
+
ROM Reference: src/act_info.c get_char_room()
|
|
252
|
+
Note: ROM allows finding self - individual commands check victim == ch.
|
|
253
|
+
"""
|
|
249
254
|
room = getattr(char, "room", None)
|
|
250
255
|
if room is None or not name:
|
|
251
256
|
return None
|
|
252
257
|
|
|
253
258
|
lowered = name.lower()
|
|
254
259
|
for candidate in getattr(room, "people", []) or []:
|
|
255
|
-
if candidate is char:
|
|
256
|
-
continue
|
|
257
260
|
candidate_name = getattr(candidate, "name", "") or ""
|
|
258
261
|
if lowered in candidate_name.lower():
|
|
259
262
|
return candidate
|
|
@@ -529,19 +532,42 @@ def do_berserk(char: Character, args: str) -> str:
|
|
|
529
532
|
|
|
530
533
|
|
|
531
534
|
def do_surrender(char: Character, args: str) -> str:
|
|
535
|
+
"""
|
|
536
|
+
Surrender to opponent, ending combat.
|
|
537
|
+
|
|
538
|
+
ROM Reference: src/fight.c do_surrender (lines 3222-3242)
|
|
539
|
+
|
|
540
|
+
Allows a character to yield to their opponent. If surrendering to an NPC,
|
|
541
|
+
the NPC's TRIG_SURR mobprog is triggered if present. By default, NPCs
|
|
542
|
+
ignore surrender and continue attacking.
|
|
543
|
+
"""
|
|
544
|
+
# Must be fighting (ROM fight.c:3225-3229)
|
|
532
545
|
opponent = getattr(char, "fighting", None)
|
|
533
546
|
if opponent is None:
|
|
534
|
-
return "But you're not fighting
|
|
547
|
+
return "But you're not fighting!\n\r"
|
|
548
|
+
|
|
549
|
+
# Messages (ROM fight.c:3230-3232)
|
|
550
|
+
opponent_name = getattr(opponent, "name", "someone")
|
|
551
|
+
messages = [f"You surrender to {opponent_name}!"]
|
|
535
552
|
|
|
553
|
+
# Stop fighting (ROM fight.c:3233 - stop_fighting(ch, TRUE))
|
|
536
554
|
stop_fighting(char, True)
|
|
537
555
|
if getattr(opponent, "fighting", None) is char:
|
|
538
556
|
opponent.fighting = None
|
|
539
557
|
|
|
558
|
+
# Check for TRIG_SURR mobprog if player surrendering to NPC
|
|
559
|
+
# ROM fight.c:3235-3241
|
|
540
560
|
if not getattr(char, "is_npc", False) and getattr(opponent, "is_npc", False):
|
|
561
|
+
# Try to trigger surrender mobprog
|
|
541
562
|
if not mobprog.mp_surr_trigger(opponent, char):
|
|
542
|
-
|
|
563
|
+
# No mobprog or mobprog didn't handle it - NPC ignores and attacks
|
|
564
|
+
messages.append(f"{opponent_name} seems to ignore your cowardly act!")
|
|
565
|
+
# ROM: multi_hit(mob, ch, TYPE_UNDEFINED)
|
|
566
|
+
attack_messages = multi_hit(opponent, char)
|
|
567
|
+
if attack_messages:
|
|
568
|
+
messages.extend(attack_messages)
|
|
543
569
|
|
|
544
|
-
return "
|
|
570
|
+
return "\n".join(messages) if len(messages) > 1 else messages[0]
|
|
545
571
|
|
|
546
572
|
|
|
547
573
|
def do_flee(char: Character, args: str) -> str:
|
|
@@ -571,10 +597,10 @@ def do_flee(char: Character, args: str) -> str:
|
|
|
571
597
|
dex = char.get_curr_stat(Stat.DEX)
|
|
572
598
|
else:
|
|
573
599
|
dex = 13 # Default dex
|
|
574
|
-
|
|
600
|
+
|
|
575
601
|
if dex is None:
|
|
576
602
|
dex = 13
|
|
577
|
-
|
|
603
|
+
|
|
578
604
|
chance = 50 + (dex - 13) * 5 # Base 50%, +/- 5% per dex point
|
|
579
605
|
|
|
580
606
|
# Reduce chance if badly hurt
|
|
@@ -597,17 +623,17 @@ def do_flee(char: Character, args: str) -> str:
|
|
|
597
623
|
for direction, exit_data in exits.items():
|
|
598
624
|
if exit_data:
|
|
599
625
|
# Handle both dict-style and Exit object style
|
|
600
|
-
if hasattr(exit_data,
|
|
626
|
+
if hasattr(exit_data, "exit_info"):
|
|
601
627
|
# Exit object
|
|
602
|
-
closed = bool(getattr(exit_data,
|
|
603
|
-
to_room = getattr(exit_data,
|
|
628
|
+
closed = bool(getattr(exit_data, "exit_info", 0) & 1) # EX_ISDOOR and closed
|
|
629
|
+
to_room = getattr(exit_data, "to_room", None)
|
|
604
630
|
elif isinstance(exit_data, dict):
|
|
605
631
|
# Dict style
|
|
606
632
|
closed = exit_data.get("closed", False)
|
|
607
633
|
to_room = exit_data.get("to_room")
|
|
608
634
|
else:
|
|
609
635
|
continue
|
|
610
|
-
|
|
636
|
+
|
|
611
637
|
if not closed and to_room:
|
|
612
638
|
valid_exits.append((direction, to_room))
|
|
613
639
|
|
|
@@ -754,16 +780,16 @@ def do_cast(char: Character, args: str) -> str:
|
|
|
754
780
|
def do_dirt(char: Character, args: str) -> str:
|
|
755
781
|
"""
|
|
756
782
|
Kick dirt in opponent's eyes to blind them.
|
|
757
|
-
|
|
783
|
+
|
|
758
784
|
ROM Reference: src/fight.c do_dirt (lines 2489-2640)
|
|
759
785
|
"""
|
|
760
786
|
target_name = (args or "").strip()
|
|
761
|
-
|
|
787
|
+
|
|
762
788
|
# Check if character has the skill
|
|
763
789
|
skill_level = char.skills.get("dirt kicking", 0)
|
|
764
790
|
if skill_level == 0:
|
|
765
791
|
return "You get your feet dirty."
|
|
766
|
-
|
|
792
|
+
|
|
767
793
|
# Find target
|
|
768
794
|
if not target_name:
|
|
769
795
|
victim = getattr(char, "fighting", None)
|
|
@@ -773,63 +799,38 @@ def do_dirt(char: Character, args: str) -> str:
|
|
|
773
799
|
victim = _find_room_target(char, target_name)
|
|
774
800
|
if victim is None:
|
|
775
801
|
return "They aren't here."
|
|
776
|
-
|
|
777
|
-
#
|
|
802
|
+
|
|
803
|
+
# Match ROM safety/validation gates in the command layer.
|
|
804
|
+
if victim is char:
|
|
805
|
+
return "Very funny."
|
|
806
|
+
|
|
778
807
|
victim_affected = getattr(victim, "affected_by", 0)
|
|
779
808
|
if victim_affected & AffectFlag.BLIND:
|
|
780
809
|
return "They're already blinded."
|
|
781
|
-
|
|
782
|
-
if victim is char:
|
|
783
|
-
return "Very funny."
|
|
784
|
-
|
|
785
|
-
# Safety checks
|
|
810
|
+
|
|
786
811
|
safety_msg = _kill_safety_message(char, victim)
|
|
787
812
|
if safety_msg:
|
|
788
813
|
return safety_msg
|
|
789
|
-
|
|
790
|
-
#
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
chance += char_dex - 2 * victim_dex
|
|
795
|
-
|
|
796
|
-
# Level modifier
|
|
797
|
-
char_level = skill_handlers._coerce_int(getattr(char, "level", 1))
|
|
798
|
-
victim_level = skill_handlers._coerce_int(getattr(victim, "level", 1))
|
|
799
|
-
chance += (char_level - victim_level) * 2
|
|
800
|
-
|
|
801
|
-
# Roll
|
|
802
|
-
if rng_mm.number_percent() < chance:
|
|
803
|
-
# Success - blind the victim
|
|
804
|
-
victim.affected_by = victim_affected | AffectFlag.BLIND
|
|
805
|
-
skill_registry._apply_wait_state(char, get_pulse_violence())
|
|
806
|
-
|
|
807
|
-
# Start combat if not already fighting
|
|
808
|
-
if not getattr(char, "fighting", None):
|
|
809
|
-
char.fighting = victim
|
|
810
|
-
if not getattr(victim, "fighting", None):
|
|
811
|
-
victim.fighting = char
|
|
812
|
-
|
|
813
|
-
check_killer(char, victim)
|
|
814
|
-
return f"You kick dirt into {getattr(victim, 'name', 'their')} eyes!"
|
|
815
|
-
else:
|
|
816
|
-
skill_registry._apply_wait_state(char, get_pulse_violence())
|
|
817
|
-
return "You kick dirt but miss their eyes."
|
|
814
|
+
|
|
815
|
+
# Delegate parity math/effects to the skill handler.
|
|
816
|
+
result = skill_handlers.dirt_kicking(char, target=victim)
|
|
817
|
+
check_killer(char, victim)
|
|
818
|
+
return result
|
|
818
819
|
|
|
819
820
|
|
|
820
821
|
def do_trip(char: Character, args: str) -> str:
|
|
821
822
|
"""
|
|
822
823
|
Trip opponent to knock them down.
|
|
823
|
-
|
|
824
|
+
|
|
824
825
|
ROM Reference: src/fight.c do_trip (lines 2641-2760)
|
|
825
826
|
"""
|
|
826
827
|
target_name = (args or "").strip()
|
|
827
|
-
|
|
828
|
+
|
|
828
829
|
# Check if character has the skill
|
|
829
830
|
skill_level = char.skills.get("trip", 0)
|
|
830
831
|
if skill_level == 0:
|
|
831
832
|
return "Tripping? What's that?"
|
|
832
|
-
|
|
833
|
+
|
|
833
834
|
# Find target
|
|
834
835
|
if not target_name:
|
|
835
836
|
victim = getattr(char, "fighting", None)
|
|
@@ -839,56 +840,60 @@ def do_trip(char: Character, args: str) -> str:
|
|
|
839
840
|
victim = _find_room_target(char, target_name)
|
|
840
841
|
if victim is None:
|
|
841
842
|
return "They aren't here."
|
|
842
|
-
|
|
843
|
+
|
|
843
844
|
# Safety checks
|
|
844
845
|
safety_msg = _kill_safety_message(char, victim)
|
|
845
846
|
if safety_msg:
|
|
846
847
|
return safety_msg
|
|
847
|
-
|
|
848
|
+
|
|
848
849
|
# Can't trip flying targets
|
|
849
850
|
victim_affected = getattr(victim, "affected_by", 0)
|
|
850
851
|
if victim_affected & AffectFlag.FLYING:
|
|
851
852
|
return "Their feet aren't on the ground."
|
|
852
|
-
|
|
853
|
+
|
|
853
854
|
# Can't trip someone already down
|
|
854
855
|
victim_pos = getattr(victim, "position", Position.STANDING)
|
|
855
856
|
if victim_pos < Position.FIGHTING:
|
|
856
857
|
return "They are already down."
|
|
857
|
-
|
|
858
|
+
|
|
858
859
|
if victim is char:
|
|
859
860
|
skill_registry._apply_wait_state(char, get_pulse_violence() * 2)
|
|
860
861
|
return "You fall flat on your face!"
|
|
861
|
-
|
|
862
|
+
|
|
862
863
|
# Calculate chance
|
|
863
864
|
chance = skill_level
|
|
864
|
-
|
|
865
|
+
|
|
865
866
|
# Size modifier
|
|
866
867
|
char_size = skill_handlers._coerce_int(getattr(char, "size", 2))
|
|
867
868
|
victim_size = skill_handlers._coerce_int(getattr(victim, "size", 2))
|
|
868
869
|
if char_size < victim_size:
|
|
869
870
|
chance += (char_size - victim_size) * 10
|
|
870
|
-
|
|
871
|
+
|
|
871
872
|
# Dex modifier
|
|
872
|
-
char_dex = skill_handlers._coerce_int(
|
|
873
|
-
|
|
873
|
+
char_dex = skill_handlers._coerce_int(
|
|
874
|
+
getattr(char, "perm_stat", [13] * 5)[1] if isinstance(getattr(char, "perm_stat", []), list) else 13
|
|
875
|
+
)
|
|
876
|
+
victim_dex = skill_handlers._coerce_int(
|
|
877
|
+
getattr(victim, "perm_stat", [13] * 5)[1] if isinstance(getattr(victim, "perm_stat", []), list) else 13
|
|
878
|
+
)
|
|
874
879
|
chance += char_dex - victim_dex * 3 // 2
|
|
875
|
-
|
|
880
|
+
|
|
876
881
|
# Level modifier
|
|
877
882
|
char_level = skill_handlers._coerce_int(getattr(char, "level", 1))
|
|
878
883
|
victim_level = skill_handlers._coerce_int(getattr(victim, "level", 1))
|
|
879
884
|
chance += (char_level - victim_level) * 2
|
|
880
|
-
|
|
885
|
+
|
|
881
886
|
# Roll
|
|
882
887
|
if rng_mm.number_percent() < chance:
|
|
883
888
|
# Success
|
|
884
889
|
victim.position = Position.RESTING
|
|
885
890
|
skill_registry._apply_wait_state(char, get_pulse_violence())
|
|
886
891
|
skill_registry._apply_wait_state(victim, get_pulse_violence() * 2)
|
|
887
|
-
|
|
892
|
+
|
|
888
893
|
# Damage
|
|
889
894
|
damage_amt = rng_mm.number_range(2, 2 + 2 * victim_size + skill_level // 20)
|
|
890
895
|
apply_damage(char, victim, damage_amt, DamageType.BASH)
|
|
891
|
-
|
|
896
|
+
|
|
892
897
|
check_killer(char, victim)
|
|
893
898
|
return f"You trip {getattr(victim, 'name', 'them')} and they go down!"
|
|
894
899
|
else:
|
|
@@ -899,67 +904,34 @@ def do_trip(char: Character, args: str) -> str:
|
|
|
899
904
|
def do_disarm(char: Character, args: str) -> str:
|
|
900
905
|
"""
|
|
901
906
|
Attempt to disarm opponent's weapon.
|
|
902
|
-
|
|
907
|
+
|
|
903
908
|
ROM Reference: src/fight.c do_disarm (lines 3145-3220)
|
|
904
909
|
"""
|
|
905
910
|
# Check if character has the skill
|
|
906
911
|
skill_level = char.skills.get("disarm", 0)
|
|
907
912
|
if skill_level == 0:
|
|
908
913
|
return "You don't know how to disarm opponents."
|
|
909
|
-
|
|
914
|
+
|
|
910
915
|
# Must be fighting
|
|
911
916
|
victim = getattr(char, "fighting", None)
|
|
912
917
|
if victim is None:
|
|
913
918
|
return "You aren't fighting anyone."
|
|
914
|
-
|
|
919
|
+
|
|
920
|
+
# Attacker must have a weapon, or meet ROM hand-to-hand / NPC OFF_DISARM exception.
|
|
921
|
+
caster_weapon = get_wielded_weapon(char)
|
|
922
|
+
hth_skill = char.skills.get("hand to hand", 0)
|
|
923
|
+
if caster_weapon is None and hth_skill == 0:
|
|
924
|
+
return "You must wield a weapon to disarm."
|
|
925
|
+
|
|
915
926
|
# Victim must be wielding a weapon
|
|
916
|
-
|
|
917
|
-
victim_weapon = victim_equipped.get("wield") or victim_equipped.get("main_hand")
|
|
927
|
+
victim_weapon = get_wielded_weapon(victim)
|
|
918
928
|
if victim_weapon is None:
|
|
919
929
|
return "Your opponent is not wielding a weapon."
|
|
920
|
-
|
|
921
|
-
# Attacker should have weapon (or hand-to-hand skill)
|
|
922
|
-
char_equipped = getattr(char, "equipped", {})
|
|
923
|
-
char_weapon = char_equipped.get("wield") or char_equipped.get("main_hand")
|
|
924
|
-
hth_skill = char.skills.get("hand to hand", 0)
|
|
925
|
-
|
|
926
|
-
if char_weapon is None and hth_skill == 0:
|
|
927
|
-
return "You must wield a weapon to disarm."
|
|
928
|
-
|
|
929
|
-
# Calculate chance
|
|
930
|
-
if char_weapon is None:
|
|
931
|
-
chance = skill_level * hth_skill // 150
|
|
932
|
-
else:
|
|
933
|
-
chance = skill_level
|
|
934
|
-
|
|
935
|
-
# Dex vs Str
|
|
936
|
-
char_dex = skill_handlers._coerce_int(getattr(char, "perm_stat", [13]*5)[1] if isinstance(getattr(char, "perm_stat", []), list) else 13)
|
|
937
|
-
victim_str = skill_handlers._coerce_int(getattr(victim, "perm_stat", [13]*5)[0] if isinstance(getattr(victim, "perm_stat", []), list) else 13)
|
|
938
|
-
chance += char_dex - 2 * victim_str
|
|
939
|
-
|
|
940
|
-
# Level modifier
|
|
941
|
-
char_level = skill_handlers._coerce_int(getattr(char, "level", 1))
|
|
942
|
-
victim_level = skill_handlers._coerce_int(getattr(victim, "level", 1))
|
|
943
|
-
chance += (char_level - victim_level) * 2
|
|
944
|
-
|
|
945
|
-
skill_registry._apply_wait_state(char, get_pulse_violence())
|
|
946
|
-
|
|
947
|
-
# Roll
|
|
948
|
-
if rng_mm.number_percent() < chance:
|
|
949
|
-
# Success - remove weapon from victim
|
|
950
|
-
if "wield" in victim_equipped:
|
|
951
|
-
del victim_equipped["wield"]
|
|
952
|
-
elif "main_hand" in victim_equipped:
|
|
953
|
-
del victim_equipped["main_hand"]
|
|
954
|
-
|
|
955
|
-
# Drop to room
|
|
956
|
-
victim_room = getattr(victim, "room", None)
|
|
957
|
-
if victim_room and hasattr(victim_room, "contents"):
|
|
958
|
-
victim_room.contents.append(victim_weapon)
|
|
959
|
-
victim_weapon.in_room = victim_room
|
|
960
|
-
|
|
961
|
-
check_killer(char, victim)
|
|
962
|
-
return f"You disarm {getattr(victim, 'name', 'them')}!"
|
|
963
|
-
else:
|
|
964
|
-
return f"You fail to disarm {getattr(victim, 'name', 'them')}."
|
|
965
930
|
|
|
931
|
+
success = skill_handlers.disarm(char, target=victim)
|
|
932
|
+
check_killer(char, victim)
|
|
933
|
+
|
|
934
|
+
victim_name = getattr(victim, "name", "them")
|
|
935
|
+
if success:
|
|
936
|
+
return f"You disarm {victim_name}!"
|
|
937
|
+
return f"You fail to disarm {victim_name}."
|